diff --git a/Makefile.am b/Makefile.am index 0904c7993..91df7c847 100644 --- a/Makefile.am +++ b/Makefile.am @@ -249,7 +249,7 @@ endif # Enable image support. if ENABLE_IMAGES -dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c +dist_tmux_SOURCES += image.c image-sixel.c image-kitty.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..6ff7a5011 --- /dev/null +++ b/image-fallback.c @@ -0,0 +1,464 @@ +/* $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 +#include + +#include "tmux.h" + +/* Character-cell fallback for clients without a graphical image protocol. */ + +enum image_glyph_detail { + IMAGE_GLYPH_ASCII, + IMAGE_GLYPH_SHADE5, + IMAGE_GLYPH_SHADE8, + IMAGE_GLYPH_HALF, + IMAGE_GLYPH_QUADRANT, + IMAGE_GLYPH_SEXTANT +}; + +enum image_glyph_palette { + IMAGE_GLYPH_PALETTE_8, + IMAGE_GLYPH_PALETTE_16, + IMAGE_GLYPH_PALETTE_256, + IMAGE_GLYPH_PALETTE_RGB +}; + +struct image_rgb { + u_char r; + u_char g; + u_char b; +}; + +struct image_glyph_data { + u_char *shade5; + u_char *shade8; +}; + +static const struct image_rgb image_ansi_colours[16] = { + { 0x00, 0x00, 0x00 }, { 0x80, 0x00, 0x00 }, + { 0x00, 0x80, 0x00 }, { 0x80, 0x80, 0x00 }, + { 0x00, 0x00, 0x80 }, { 0x80, 0x00, 0x80 }, + { 0x00, 0x80, 0x80 }, { 0xc0, 0xc0, 0xc0 }, + { 0x80, 0x80, 0x80 }, { 0xff, 0x00, 0x00 }, + { 0x00, 0xff, 0x00 }, { 0xff, 0xff, 0x00 }, + { 0x00, 0x00, 0xff }, { 0xff, 0x00, 0xff }, + { 0x00, 0xff, 0xff }, { 0xff, 0xff, 0xff } +}; + +static u_int +image_glyph_distance(struct image_rgb a, struct image_rgb b) +{ + int r = a.r - b.r, g = a.g - b.g, bl = a.b - b.b; + + return (r * r + g * g + bl * bl); +} + +static int +image_glyph_low_saturation(struct image_rgb colour) +{ + u_int minimum, maximum; + + minimum = maximum = colour.r; + if (colour.g < minimum) + minimum = colour.g; + if (colour.b < minimum) + minimum = colour.b; + if (colour.g > maximum) + maximum = colour.g; + if (colour.b > maximum) + maximum = colour.b; + return (maximum == 0 || (maximum - minimum) * 4 <= maximum); +} + +static u_int +image_glyph_nearest_ansi(struct image_rgb colour, u_int colours) +{ + u_int i, distance, best_distance = UINT_MAX, best = 0; + int neutral = image_glyph_low_saturation(colour); + + for (i = 0; i < colours; i++) { + if (neutral && i != 0 && i != 7 && + (colours != 16 || (i != 8 && i != 15))) + continue; + distance = image_glyph_distance(colour, image_ansi_colours[i]); + if (distance < best_distance) { + best_distance = distance; + best = i; + } + } + return (best); +} + +static struct image_rgb +image_glyph_quantize(struct image_rgb colour, enum image_glyph_palette palette, + int *output) +{ + struct image_rgb result; + int value; + u_int index; + + if (palette == IMAGE_GLYPH_PALETTE_RGB) { + *output = colour_join_rgb(colour.r, colour.g, colour.b); + return (colour); + } + if (palette == IMAGE_GLYPH_PALETTE_256) { + *output = colour_find_rgb(colour.r, colour.g, colour.b); + value = colour_256toRGB(*output); + 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); + result = image_ansi_colours[index]; + if (index < 8) + *output = index; + else + *output = 90 + index - 8; + return (result); +} + +static void +image_glyph_fit_colours(const struct image_rgb *samples, u_int count, + struct image_rgb centres[2]) +{ + u_int sum[2][3], counts[2], group, i, j, iteration, distance; + u_int maximum = 0, first = 0, second = 0; + + for (i = 0; i < count; i++) { + for (j = i + 1; j < count; j++) { + distance = image_glyph_distance(samples[i], samples[j]); + if (distance > maximum) { + maximum = distance; + first = i; + second = j; + } + } + } + centres[0] = samples[first]; + centres[1] = samples[second]; + for (iteration = 0; iteration < 4; iteration++) { + memset(sum, 0, sizeof sum); + memset(counts, 0, sizeof counts); + for (i = 0; i < count; i++) { + group = image_glyph_distance(samples[i], centres[1]) < + image_glyph_distance(samples[i], centres[0]); + sum[group][0] += samples[i].r; + sum[group][1] += samples[i].g; + sum[group][2] += samples[i].b; + counts[group]++; + } + for (i = 0; i < 2; i++) { + if (counts[i] == 0) + continue; + centres[i].r = (sum[i][0] + counts[i] / 2) / counts[i]; + centres[i].g = (sum[i][1] + counts[i] / 2) / counts[i]; + centres[i].b = (sum[i][2] + counts[i] / 2) / counts[i]; + } + } +} + +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); + + if (s == NULL) + return (0); + ud = utf8_fromcstr(s); + if (ud[0].size == 0) { + free(ud); + return (0); + } + memcpy(data, &ud[0], sizeof *data); + free(ud); + return (1); +} + +static u_char +image_glyph_block_key(enum image_glyph_detail detail, u_int mask) +{ + static const u_char half[4] = { + 0, TTY_ACS_IMAGE_HALF_UPPER, TTY_ACS_IMAGE_HALF_LOWER, + TTY_ACS_IMAGE_BLOCK + }; + static const u_char quadrant[16] = { + 0, TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT, + TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, TTY_ACS_IMAGE_HALF_UPPER, + TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, TTY_ACS_IMAGE_HALF_LEFT, + TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT, + TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT, + TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT, + TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT, + TTY_ACS_IMAGE_HALF_RIGHT, + TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT, + TTY_ACS_IMAGE_HALF_LOWER, + TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT, + TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT, + TTY_ACS_IMAGE_BLOCK + }; + + if (detail == IMAGE_GLYPH_HALF) + return (half[mask]); + if (detail == IMAGE_GLYPH_QUADRANT) + return (quadrant[mask]); + if (mask == 0) + return (0); + if (mask == 21) + return (TTY_ACS_IMAGE_HALF_LEFT); + if (mask == 42) + return (TTY_ACS_IMAGE_HALF_RIGHT); + if (mask == 63) + return (TTY_ACS_IMAGE_BLOCK); + return (tty_acs_image_sextant(mask)); +} + +static u_char * +image_glyph_make_shades(struct image *im, u_int levels) +{ + struct image_glyph_data *data = im->fallback_data; + const struct image_cell *cell; + int *values, value, represented, error; + size_t cells, index; + u_int x, y, scan, level, minimum = 255, maximum = 0; + int reverse; + u_char *result; + + if (data == NULL) { + data = im->fallback_data = xcalloc(1, sizeof *data); + } + result = (levels == 5 ? data->shade5 : data->shade8); + if (result != NULL) + return (result); + cells = (size_t)im->sx * im->sy; + values = xcalloc(cells, sizeof *values); + result = xcalloc(cells, 1); + for (y = 0; y < im->sy; y++) { + for (x = 0; x < im->sx; x++) { + index = (size_t)y * im->sx + x; + cell = image_get_cell(im, x, y); + value = cell->whole.brightness; + if ((u_int)value < minimum) + minimum = value; + if ((u_int)value > maximum) + maximum = value; + values[index] = value; + } + } + if (maximum > minimum) { + for (index = 0; index < cells; index++) + values[index] = (values[index] - minimum) * 255 / + (maximum - minimum); + } + for (y = 0; y < im->sy; y++) { + reverse = (y & 1); + for (scan = 0; scan < im->sx; scan++) { + x = (reverse ? im->sx - scan - 1 : scan); + index = (size_t)y * im->sx + x; + value = values[index]; + if (value < 0) + value = 0; + if (value > 255) + value = 255; + level = (value * (levels - 1) + 127) / 255; + result[index] = level; + represented = level * 255 / (levels - 1); + error = value - represented; + if (!reverse && x + 1 < im->sx) + values[index + 1] += error * 7 / 16; + if (reverse && x != 0) + values[index - 1] += error * 7 / 16; + if (y + 1 == im->sy) + continue; + if (!reverse && x != 0) + values[index + im->sx - 1] += error * 3 / 16; + if (reverse && x + 1 < im->sx) + values[index + im->sx + 1] += error * 3 / 16; + values[index + im->sx] += error * 5 / 16; + if (!reverse && x + 1 < im->sx) + values[index + im->sx + 1] += error / 16; + if (reverse && x != 0) + values[index + im->sx - 1] += error / 16; + } + } + free(values); + if (levels == 5) + data->shade5 = result; + else + data->shade8 = result; + return (result); +} + +static enum image_glyph_palette +image_glyph_get_palette(struct tty *tty) +{ + int colours; + + if (tty->term->flags & TERM_RGBCOLOURS) + return (IMAGE_GLYPH_PALETTE_RGB); + if (tty->term->flags & TERM_256COLOURS) + return (IMAGE_GLYPH_PALETTE_256); + colours = tty_term_number(tty->term, TTYC_COLORS); + if (colours >= 256) + return (IMAGE_GLYPH_PALETTE_256); + if (colours >= 16) + return (IMAGE_GLYPH_PALETTE_16); + return (IMAGE_GLYPH_PALETTE_8); +} + +static enum image_glyph_detail +image_glyph_get_detail(struct tty *tty, enum image_glyph_palette palette) +{ + if (tty_acs_needed(tty)) + return (IMAGE_GLYPH_ASCII); + if (tty->term->flags & TERM_IMAGE_SEXTANTS) + return (IMAGE_GLYPH_SEXTANT); + if (tty->term->flags & TERM_IMAGE_QUADRANTS) + return (IMAGE_GLYPH_QUADRANT); + if (palette != IMAGE_GLYPH_PALETTE_8) + return (IMAGE_GLYPH_HALF); + if (tty_term_has(tty->term, TTYC_NOBR)) + return (IMAGE_GLYPH_SHADE5); + return (IMAGE_GLYPH_SHADE8); +} + +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, + struct grid_cell *out) +{ + const struct image_cell *cell = image_get_cell(im, x, y); + struct image_rgb samples[6], centres[2], quantized[2]; + u_int columns, rows, sx, sy, ix, iy, i, n, mask; + u_int x0, x1, y0, y1, red, green, blue, count; + int colours[2]; + u_char key; + + columns = (detail == IMAGE_GLYPH_HALF ? 1 : 2); + rows = (detail == IMAGE_GLYPH_HALF ? 2 : + detail == IMAGE_GLYPH_QUADRANT ? 2 : 3); + i = 0; + for (sy = 0; sy < rows; sy++) { + for (sx = 0; sx < columns; sx++) { + x0 = sx * IMAGE_SAMPLE_COLUMNS / columns; + x1 = (sx + 1) * IMAGE_SAMPLE_COLUMNS / columns; + y0 = sy * IMAGE_SAMPLE_ROWS / rows; + y1 = (sy + 1) * IMAGE_SAMPLE_ROWS / rows; + red = green = blue = count = 0; + for (iy = y0; iy < y1; iy++) { + for (ix = x0; ix < x1; ix++) { + red += cell->samples[iy][ix].red; + green += cell->samples[iy][ix].green; + blue += cell->samples[iy][ix].blue; + count++; + } + } + samples[i].r = red / count; + samples[i].g = green / count; + samples[i].b = blue / count; + i++; + } + } + n = columns * rows; + image_glyph_fit_colours(samples, n, centres); + quantized[0] = image_glyph_quantize(centres[0], palette, &colours[0]); + quantized[1] = image_glyph_quantize(centres[1], palette, &colours[1]); + mask = 0; + for (i = 0; i < n; i++) { + if (image_glyph_distance(samples[i], quantized[1]) < + image_glyph_distance(samples[i], quantized[0])) + mask |= (1U << i); + } + key = image_glyph_block_key(detail, mask); + if (key == 0) + utf8_set(&out->data, ' '); + else if (!image_glyph_set_acs(tty, &out->data, key)) + utf8_set(&out->data, ' '); + out->fg = colours[1]; + out->bg = colours[0]; +} + +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, + __unused const struct tty_style_ctx *style_ctx) +{ + static const char ascii[] = " .:-=+*#%@"; + static const u_char shades[5] = { 0, TTY_ACS_IMAGE_SHADE_LIGHT, + TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_DARK, + TTY_ACS_IMAGE_BLOCK }; + static const u_char bold_shades[8] = { 0, + TTY_ACS_IMAGE_SHADE_LIGHT, TTY_ACS_IMAGE_SHADE_LIGHT, + TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_MEDIUM, + TTY_ACS_IMAGE_SHADE_DARK, TTY_ACS_IMAGE_BLOCK, + TTY_ACS_IMAGE_BLOCK }; + const struct image_cell *cell; + enum image_glyph_palette palette; + enum image_glyph_detail detail; + u_char *levels, key; + u_int level = 0; + + memcpy(out, gc, sizeof *out); + cell = image_get_cell(im, x, y); + if (cell == NULL) { + utf8_set(&out->data, ' '); + return; + } + palette = image_glyph_get_palette(tty); + detail = image_glyph_get_detail(tty, palette); + if (detail == IMAGE_GLYPH_ASCII) { + level = cell->whole.brightness * (sizeof ascii - 2) / 255; + utf8_set(&out->data, ascii[level]); + return; + } + if (detail == IMAGE_GLYPH_SHADE5 || detail == IMAGE_GLYPH_SHADE8) { + levels = image_glyph_make_shades(im, + detail == IMAGE_GLYPH_SHADE5 ? 5 : 8); + level = levels[(size_t)y * im->sx + x]; + key = (detail == IMAGE_GLYPH_SHADE5 ? shades[level] : + bold_shades[level]); + if (key == 0) + utf8_set(&out->data, ' '); + else if (!image_glyph_set_acs(tty, &out->data, key)) + utf8_set(&out->data, ' '); + out->fg = 7; + out->bg = 0; + out->attr &= ~GRID_ATTR_BRIGHT; + if (detail == IMAGE_GLYPH_SHADE8 && + (level == 2 || level == 4 || level == 7)) + out->attr |= GRID_ATTR_BRIGHT; + return; + } + image_glyph_block(tty, im, x, y, detail, palette, out); +} + +void +image_free_fallback(struct image *im) +{ + struct image_glyph_data *data = im->fallback_data; + + if (data == NULL) + return; + free(data->shade5); + free(data->shade8); + free(data); +} diff --git a/image.c b/image.c index 74a7cfe7b..979b0e967 100644 --- a/image.c +++ b/image.c @@ -27,48 +27,8 @@ #include "tmux.h" -/* An average of part of an image cell. RGB is premultiplied. */ -struct image_sample { - u_char red; - u_char green; - u_char blue; - u_char alpha; - u_char brightness; -}; - -/* Half blocks, quadrants and sextants all divide evenly into a 2 by 6 grid. */ -#define IMAGE_SAMPLE_COLUMNS 2 -#define IMAGE_SAMPLE_ROWS 6 #define IMAGE_FLAG_NO_CURSOR 0x1 #define IMAGE_Z_BELOW_BACKGROUND (INT32_MIN / 2) -struct image_cell { - struct image_sample whole; - struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS]; -}; - -/* Immutable image data and cell geometry. */ -struct image { - u_int id; - u_int references; - u_int flags; - u_int parent_id; - u_int source_id; - u_int width; - u_int height; - u_int canvas_width; - u_int canvas_height; - u_int sx; - u_int sy; - size_t stride; - size_t size; - u_char *pixels; - /* Original indexed SIXEL data, if this image arrived as SIXEL. */ - struct sixel_image *sixel; - struct image_cell *cells; - - RB_ENTRY(image) entry; -}; -RB_HEAD(images, image); /* A cell-aligned part of an image to draw at a terminal position. */ struct image_rect { @@ -1055,7 +1015,7 @@ image_free(u_int id) } /* Return a precomputed fallback cell sample. */ -static const struct image_cell * +const struct image_cell * image_get_cell(struct image *im, u_int x, u_int y) { if (im == NULL || x >= im->sx || y >= im->sy) @@ -1065,23 +1025,6 @@ image_get_cell(struct image *im, u_int x, u_int y) return (&im->cells[(size_t)y * im->sx + x]); } -/* Fill a grid cell with a fallback image glyph. */ -void -image_get_fallback_cell(__unused struct tty *tty, struct image *im, u_int x, - u_int y, const struct grid_cell *gc, struct grid_cell *out, - __unused const struct tty_style_ctx *style_ctx) -{ - static const char ramp[] = " .:-=+*#%@"; - const struct image_cell *cell; - u_int level = 0; - - memcpy(out, gc, sizeof *out); - cell = image_get_cell(im, x, y); - if (cell != NULL) - level = cell->whole.brightness * (sizeof ramp - 2) / 255; - utf8_set(&out->data, ramp[level]); -} - /* Return one for a fallback cell, minus one to continue along an image line. */ int image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, diff --git a/tmux.1 b/tmux.1 index 05873a8a1..b67f2cde8 100644 --- a/tmux.1 +++ b/tmux.1 @@ -5070,6 +5070,10 @@ Supports extended keys. Supports focus reporting. .It hyperlinks Supports OSC 8 hyperlinks. +.It image-quadrants +Allows image fallbacks to use Unicode quadrant block characters. +.It image-sextants +Allows image fallbacks to use Unicode sextant block characters. .It ignorefkeys Ignore function keys from .Xr terminfo 5 diff --git a/tmux.h b/tmux.h index 16b605027..dd46ea6c9 100644 --- a/tmux.h +++ b/tmux.h @@ -1034,6 +1034,63 @@ struct style { }; #ifdef ENABLE_IMAGES +/* Private ACS keys used by the text image backend. */ +#define TTY_ACS_IMAGE_SHADE_LIGHT 0x80 +#define TTY_ACS_IMAGE_SHADE_MEDIUM 0x81 +#define TTY_ACS_IMAGE_SHADE_DARK 0x82 +#define TTY_ACS_IMAGE_BLOCK 0x83 +#define TTY_ACS_IMAGE_HALF_UPPER 0x84 +#define TTY_ACS_IMAGE_HALF_LOWER 0x85 +#define TTY_ACS_IMAGE_HALF_LEFT 0x86 +#define TTY_ACS_IMAGE_HALF_RIGHT 0x87 +#define TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT 0x88 +#define TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT 0x89 +#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT 0x8a +#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT 0x8b +#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT 0x8c +#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT 0x8d +#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT 0x8e +#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT 0x8f +#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT 0x90 +#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT 0x91 +#define TTY_ACS_IMAGE_SEXTANT_FIRST 0x92 +#define TTY_ACS_IMAGE_SEXTANT_LAST 0xcd + +struct image_sample { + u_char red; + u_char green; + u_char blue; + u_char alpha; + u_char brightness; +}; +#define IMAGE_SAMPLE_COLUMNS 2 +#define IMAGE_SAMPLE_ROWS 6 +struct image_cell { + struct image_sample whole; + struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS]; +}; +struct image { + u_int id; + u_int references; + u_int flags; + u_int parent_id; + u_int source_id; + u_int width; + u_int height; + u_int canvas_width; + u_int canvas_height; + u_int sx; + u_int sy; + size_t stride; + size_t size; + u_char *pixels; + struct sixel_image *sixel; + struct image_cell *cells; + void *fallback_data; + + RB_ENTRY(image) entry; +}; +RB_HEAD(images, image); #define IMAGE_SIZE_LIMIT (64 * 1024 * 1024) #endif @@ -1731,6 +1788,10 @@ struct tty_term { #define TERM_SIXEL 0x40 #define TERM_INVALIDMS 0x80 #define TERM_KITTY 0x100 +#ifdef ENABLE_IMAGES +#define TERM_IMAGE_QUADRANTS 0x200 +#define TERM_IMAGE_SEXTANTS 0x400 +#endif int flags; LIST_ENTRY(tty_term) entry; @@ -3034,6 +3095,7 @@ void tty_default_features(struct client *, const char *, u_int); /* tty-acs.c */ int tty_acs_needed(struct tty *); const char *tty_acs_get(struct tty *, u_char); +u_char tty_acs_image_sextant(u_int); int tty_acs_reverse_get(struct tty *, const char *, size_t); const struct utf8_data *tty_acs_double_borders(int); const struct utf8_data *tty_acs_heavy_borders(int); @@ -4274,6 +4336,8 @@ void image_draw_line(struct tty *, struct screen *, u_int, u_int, void image_get_fallback_cell(struct tty *, struct image *, u_int, u_int, const struct grid_cell *, struct grid_cell *, const struct tty_style_ctx *); +const struct image_cell *image_get_cell(struct image *, u_int, u_int); +void image_free_fallback(struct image *); int image_get_fallback_at(struct tty *, struct screen *, u_int, u_int, const struct grid_cell *, struct grid_cell *, const struct tty_style_ctx *); diff --git a/tty-acs.c b/tty-acs.c index eedb79c2f..71455c866 100644 --- a/tty-acs.c +++ b/tty-acs.c @@ -64,9 +64,65 @@ static const struct tty_acs_entry tty_acs_table[] = { { '{', "\317\200" }, /* greek pi */ { '|', "\342\211\240" }, /* not-equal */ { '}', "\302\243" }, /* UK pound sign */ - { '~', "\302\267" } /* bullet */ + { '~', "\302\267" }, /* bullet */ +#ifdef ENABLE_IMAGES + { TTY_ACS_IMAGE_SHADE_LIGHT, "\342\226\221" }, + { TTY_ACS_IMAGE_SHADE_MEDIUM, "\342\226\222" }, + { TTY_ACS_IMAGE_SHADE_DARK, "\342\226\223" }, + { TTY_ACS_IMAGE_BLOCK, "\342\226\210" }, + { TTY_ACS_IMAGE_HALF_UPPER, "\342\226\200" }, + { TTY_ACS_IMAGE_HALF_LOWER, "\342\226\204" }, + { TTY_ACS_IMAGE_HALF_LEFT, "\342\226\214" }, + { TTY_ACS_IMAGE_HALF_RIGHT, "\342\226\220" }, + { TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, "\342\226\226" }, + { TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT, "\342\226\227" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT, "\342\226\230" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT, + "\342\226\231" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT, "\342\226\232" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT, + "\342\226\233" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT, + "\342\226\234" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, "\342\226\235" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT, "\342\226\236" }, + { TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT, + "\342\226\237" } +#endif }; +#ifdef ENABLE_IMAGES +static char tty_acs_image_sextants[60][5]; + +static void +tty_acs_image_sextants_init(void) +{ + u_int cp, i; + + if (tty_acs_image_sextants[0][0] != '\0') + return; + for (i = 0; i < nitems(tty_acs_image_sextants); i++) { + cp = 0x1fb00 + i; + tty_acs_image_sextants[i][0] = 0xf0 | (cp >> 18); + tty_acs_image_sextants[i][1] = 0x80 | ((cp >> 12) & 0x3f); + tty_acs_image_sextants[i][2] = 0x80 | ((cp >> 6) & 0x3f); + tty_acs_image_sextants[i][3] = 0x80 | (cp & 0x3f); + } +} + +u_char +tty_acs_image_sextant(u_int mask) +{ + if (mask == 0 || mask == 21 || mask == 42 || mask >= 63) + return (0); + if (mask < 21) + return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 1); + if (mask < 42) + return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 2); + return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 3); +} +#endif + /* Table mapping UTF-8 to ACS entries. */ struct tty_acs_reverse_entry { const char *string; @@ -239,6 +295,15 @@ tty_acs_get(struct tty *tty, u_char ch) return (&tty->term->acs[ch][0]); } +#ifdef ENABLE_IMAGES + if (ch >= TTY_ACS_IMAGE_SEXTANT_FIRST && + ch <= TTY_ACS_IMAGE_SEXTANT_LAST) { + tty_acs_image_sextants_init(); + return (tty_acs_image_sextants[ + ch - TTY_ACS_IMAGE_SEXTANT_FIRST]); + } +#endif + /* Otherwise look up the UTF-8 translation. */ entry = bsearch(&ch, tty_acs_table, nitems(tty_acs_table), sizeof tty_acs_table[0], tty_acs_cmp); diff --git a/tty-features.c b/tty-features.c index b92d2118f..7483852aa 100644 --- a/tty-features.c +++ b/tty-features.c @@ -371,6 +371,22 @@ static const struct tty_feature tty_feature_kitty = { TERM_KITTY }; +#ifdef ENABLE_IMAGES +static const char *const tty_feature_image_capabilities[] = { + NULL +}; +static const struct tty_feature tty_feature_image_quadrants = { + "image-quadrants", + tty_feature_image_capabilities, + TERM_IMAGE_QUADRANTS +}; +static const struct tty_feature tty_feature_image_sextants = { + "image-sextants", + tty_feature_image_capabilities, + TERM_IMAGE_SEXTANTS +}; +#endif + /* Terminal supports the OSC 9;4 progress bar. */ static const char *const tty_feature_progressbar_capabilities[] = { "Spb=\\E]9;4;%p1%d;%p2%d\\E\\\\", @@ -394,6 +410,10 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_focus, &tty_feature_ignorefkeys, &tty_feature_kitty, +#ifdef ENABLE_IMAGES + &tty_feature_image_quadrants, + &tty_feature_image_sextants, +#endif &tty_feature_margins, &tty_feature_mouse, &tty_feature_osc7,