diff --git a/Makefile.am b/Makefile.am index d79708ab6..8b06c3ac8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -252,7 +252,11 @@ endif # Enable image support. if ENABLE_IMAGES -dist_tmux_SOURCES += image.c image-grid.c image-sixel.c +dist_tmux_SOURCES += \ + image.c \ + image-grid.c \ + image-sixel.c \ + image-fallback.c endif # Add bits for fuzzing if enabled. diff --git a/image-fallback.c b/image-fallback.c new file mode 100644 index 000000000..1f0443d20 --- /dev/null +++ b/image-fallback.c @@ -0,0 +1,164 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Michael Grant + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include + +#include "tmux.h" + +/* UTF-8 characters used for image fallback. */ +static const struct utf8_data fallback_shades[5] = { + { " ", 1, 1, 1 }, + { "\342\226\221", 3, 3, 1 }, + { "\342\226\222", 3, 3, 1 }, + { "\342\226\223", 3, 3, 1 }, + { "\342\226\210", 3, 3, 1 } +}; +static const struct utf8_data fallback_half_lower = { + "\342\226\204", 3, 3, 1 +}; + +/* Return the average colour and brightness of an area. */ +static int +fallback_sample(struct image *im, uint64_t px, uint64_t py, uint64_t pw, + uint64_t ph, u_char *brightness) +{ + const u_char *pixels, *p; + uint64_t red = 0, green = 0, blue = 0, sum = 0, count; + u_int w, h, cw, ch, x0, x1, y0, y1, x, y; + size_t stride; + + image_get_size(im, &w, &h); + image_get_canvas_size(im, &cw, &ch); + pixels = image_get_pixels(im, &stride, NULL); + + x0 = px * cw / pw; + x1 = ((px + 1) * cw + pw - 1) / pw; + y0 = py * ch / ph; + y1 = ((py + 1) * ch + ph - 1) / ph; + if (x1 <= x0) + x1 = x0 + 1; + if (y1 <= y0) + y1 = y0 + 1; + if (x0 >= w || y0 >= h) { + if (brightness != NULL) + *brightness = 0; + return (colour_join_rgb(0, 0, 0)); + } + count = (uint64_t)(x1 - x0) * (y1 - y0); + + if (x1 > w) + x1 = w; + if (y1 > h) + y1 = h; + for (y = y0; y < y1; y++) { + for (x = x0; x < x1; x++) { + p = pixels + y * stride + x * 4; + + red += p[0] * p[3] / 255; + green += p[1] * p[3] / 255; + blue += p[2] * p[3] / 255; + + sum += ((2126ULL * p[0] + 7152ULL * p[1] + + 722ULL * p[2]) / 10000) * p[3] / 255; + } + } + if (brightness != NULL) + *brightness = sum / count; + return (colour_join_rgb(red / count, green / count, blue / count)); +} + +/* Set a cell to a character representing part of an image. */ +static void +fallback_get_cell(struct tty *tty, struct image *im, u_int x, u_int y, + const struct grid_cell *gc, struct grid_cell *out) +{ + static const char a[] = " .:-=+*#%@"; + static const u_char bold_shades[8] = { 0, 1, 1, 2, 2, 3, 4, 4 }; + u_int ch, level, nlevels, sx, sy, colours; + u_char brightness; + + if (out != gc) + memcpy(out, gc, sizeof *out); + out->flags &= GRID_FLAG_SELECTED; + out->attr = 0; + out->link = 0; + image_get_size_in_cells(im, &sx, &sy); + if (x >= sx || y >= sy) { + utf8_set(&out->data, ' '); + return; + } + + if (tty_acs_needed(tty)) { + fallback_sample(im, x, y, sx, sy, &brightness); + utf8_set(&out->data, a[brightness * (sizeof a - 2) / 255]); + return; + } + + if (tty->term->flags & (TERM_RGBCOLOURS|TERM_256COLOURS)) + colours = 256; + else + colours = tty_term_number(tty->term, TTYC_COLORS); + if (colours >= 16) { + out->bg = fallback_sample(im, x, y * 2, sx, sy * 2, NULL); + out->fg = fallback_sample(im, x, y * 2 + 1, sx, sy * 2, NULL); + utf8_copy(&out->data, &fallback_half_lower); + return; + } + + if (tty_term_has(tty->term, TTYC_NOBR)) + nlevels = 5; + else + nlevels = 8; + fallback_sample(im, x, y, sx, sy, &brightness); + level = (brightness * (nlevels - 1) + 127) / 255; + + if (nlevels == 8) + ch = bold_shades[level]; + else + ch = level; + utf8_copy(&out->data, &fallback_shades[ch]); + + out->fg = 7; + out->bg = 0; + out->attr &= ~GRID_ATTR_BRIGHT; + if (nlevels == 8 && (level == 2 || level == 4 || level == 7)) + out->attr |= GRID_ATTR_BRIGHT; +} + +/* Return a text cell for an image grid cell, if there is one. */ +const struct grid_cell * +fallback_map_cell(struct tty *tty, struct grid *gd, u_int x, u_int y, + const struct grid_cell *gc, struct grid_cell *out, int *empty) +{ + struct image *im; + u_int src_x, src_y; + int status; + + status = image_grid_get_at(gd, x, y, &im, &src_x, &src_y); + if (status == 1) { + fallback_get_cell(tty, im, src_x, src_y, gc, out); + if (empty != NULL) + *empty = 0; + return (out); + } + if (status == -1 && empty != NULL) + *empty = 1; + return (gc); +} diff --git a/image-grid.c b/image-grid.c index bf8e8bb38..17ee63a59 100644 --- a/image-grid.c +++ b/image-grid.c @@ -460,7 +460,7 @@ image_damage_screen(struct screen_write_ctx *ctx, u_int x, u_int y, u_int sx, image_grid_remove(s->grid, x, s->grid->hsize + y, sx, sy); } -/* Return whether an image cell has any visible pixels. */ +/* Check whether an image cell has any visible pixels. */ static int image_cell_has_visible_pixels(struct image *im, u_int x, u_int y) { @@ -569,3 +569,28 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg) image_redraw_area(ctx, cx, cy, sx, sy); screen_write_cursormove(ctx, 0, cy + sy, 0); } + +/* Find the cell within an image at a grid position. */ +int +image_grid_get_at(struct grid *gd, u_int x, u_int y, struct image **im, + u_int *src_x, u_int *src_y) +{ + struct image_line *line; + struct image_span *span, *found = NULL; + + if (y >= gd->hsize + gd->sy) + return (0); + line = gd->linedata[y].images; + if (line == NULL) + return (0); + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (x >= span->x && x < span->x + span->sx) + found = span; + } + if (found == NULL) + return (-1); + *im = found->placement->image; + *src_x = found->source_x + x - found->x; + *src_y = found->source_y; + return (1); +} diff --git a/image.c b/image.c index 5f7036612..6dc5ecb9f 100644 --- a/image.c +++ b/image.c @@ -86,7 +86,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) { @@ -139,7 +139,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 (sx * sy > IMAGE_SIZE_LIMIT) + if ((uint64_t)sx * sy > IMAGE_SIZE_LIMIT) return (NULL); im = image_alloc(w, h, cw, ch, sx, sy, w * 4, pixels); @@ -198,7 +198,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 || - view->sx * view->sy > IMAGE_SIZE_LIMIT || + (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 || @@ -255,7 +255,7 @@ image_free(struct image *im) } } -/* Return image size in pixels. */ +/* Get image size in pixels. */ void image_get_size(struct image *im, u_int *w, u_int *h) { @@ -263,7 +263,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) { @@ -271,7 +271,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) { @@ -279,7 +279,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) { diff --git a/tmux-image.h b/tmux-image.h index 2ac55c649..eebc23292 100644 --- a/tmux-image.h +++ b/tmux-image.h @@ -24,6 +24,7 @@ #error image support is not ready yet struct grid; +struct grid_cell; struct grid_line; struct image; struct image_line; @@ -31,6 +32,7 @@ struct image_store; struct screen; struct screen_write_ctx; struct sixel_image; +struct tty; /* Maximum image size in bytes. */ #define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) @@ -95,6 +97,12 @@ void image_redraw_scroll(struct screen_write_ctx *); void image_damage_screen(struct screen_write_ctx *, u_int, u_int, u_int, u_int); void image_write(struct screen_write_ctx *, struct image *, u_int); +int image_grid_get_at(struct grid *, u_int, u_int, struct image **, + u_int *, u_int *); + +/* image-fallback.c */ +const struct grid_cell *fallback_map_cell(struct tty *, struct grid *, u_int, + u_int, const struct grid_cell *, struct grid_cell *, int *); /* image-sixel.c */ #define SIXEL_COLOUR_REGISTERS 1024 diff --git a/tty-draw.c b/tty-draw.c index c5d66a169..0c08753f5 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -131,6 +131,10 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, enum tty_draw_line_state current_state, next_state; struct tty_style_ctx default_style_ctx = { 0 }; const struct grid_cell *defaults; +#ifdef ENABLE_IMAGES + struct image *im; + u_int x, y; +#endif if (style_ctx == NULL) { default_style_ctx.defaults = &grid_default_cell; @@ -188,6 +192,10 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, grid_view_get_cell(gd, i, py, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; +#ifdef ENABLE_IMAGES + if (image_grid_get_at(gd, i, gd->hsize + py, &im, &x, &y) == 1) + break; +#endif cx++; } if (cx != 0) { @@ -254,21 +262,32 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, empty = nx - i; gcp = &grid_default_cell; +#ifdef ENABLE_IMAGES + /* Map an image cell if needed. */ + gcp = fallback_map_cell(tty, gd, px + i, + gd->hsize + py, gcp, &gc, &empty); +#endif + /* Flush the background before clearing. */ if (empty != 0 && gcp->bg != last.bg) empty = 0; } else { /* Get the current cell. */ grid_view_get_cell(gd, px + i, py, &gc); + gcp = &gc; + +#ifdef ENABLE_IMAGES + /* Map an image cell if needed. */ + gcp = fallback_map_cell(tty, gd, px + i, + gd->hsize + py, gcp, &gc, NULL); +#endif /* Work out empty cells. */ - empty = tty_draw_line_get_empty(&gc, &last, + empty = tty_draw_line_get_empty(gcp, &last, nx - i); - if (empty != 0) - gcp = &gc; - else { + if (empty == 0) { /* Update for codeset if needed. */ - gcp = tty_check_codeset(tty, &gc); + gcp = tty_check_codeset(tty, gcp); /* And for selection. */ if (gcp->flags & GRID_FLAG_SELECTED) {