mirror of
https://github.com/tmux/tmux.git
synced 2026-10-15 16:04:34 +00:00
Keep retained indexed SIXEL data and the bounded per-terminal output cache. Use the cleaned-up decoder and RGBA encoder from image_merge, including parser memory limits, pixel-size handling and references for pending output. Match retained SIXEL scaling to the shared image cell footprint and emit black definitions for undefined colour registers. Keep Kitty input, output, layers and placeholders. Adapt backend selection, flags, drawing and cleanup to the callbacks and Iscrl capability. Keep the pane-status repaint with the requested XXX comment. Replace the elaborate fallback with image_merge's implementation, removing its caches, sample structures, quadrant/sextant features and ACS additions. Keep the image_merge regressions and adapt the Kitty tests to the simpler fallback. Use skip status 101 and disable Python bytecode for image tests. Small changes still outside image_merge: - Guard zero-height synchronized redraws before bit_nset. - Select the image backend when the tty opens. - Enable imagescroll by default for Kitty and Ghostty. - Keep the explanatory manual-window pixel-size comment (comment only). - Keep the additional SIXEL fixtures and visual test tools. Validation: image-enabled and disabled builds; 31 image and 13 related regressions pass normally and under ASAN. All image tests skip without images. Focused palette, parser, scaling, transparency and Kitty layer tests also pass after the retained-output adjustments. Diff checks clean.
1764 lines
48 KiB
C
1764 lines
48 KiB
C
/* $OpenBSD$ */
|
|
|
|
/*
|
|
* Copyright (c) 2026 Michael Grant <mgrant@grant.org>
|
|
*
|
|
* 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, FITNESS AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR
|
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
|
|
* THIS SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <resolv.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <zlib.h>
|
|
|
|
#include "tmux.h"
|
|
|
|
/* Return whether the terminal supports Kitty graphics. */
|
|
int
|
|
kitty_select(struct tty *tty)
|
|
{
|
|
return (tty->term != NULL &&
|
|
(tty->term->flags & TERM_KITTYGRAPHICS));
|
|
}
|
|
|
|
/* Get Kitty backend flags. */
|
|
int
|
|
kitty_get_flags(struct tty *tty)
|
|
{
|
|
int flags = IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_CLIPPED;
|
|
|
|
if (tty_term_flag(tty->term, TTYC_ISCRL))
|
|
flags |= IMAGE_BACKEND_SCROLLS;
|
|
return (flags);
|
|
}
|
|
|
|
#define KITTY_CHUNK_SIZE 3072
|
|
|
|
static const uint32_t kitty_diacritics[] = {
|
|
0x0305, 0x030D, 0x030E, 0x0310, 0x0312, 0x033D, 0x033E, 0x033F,
|
|
0x0346, 0x034A, 0x034B, 0x034C, 0x0350, 0x0351, 0x0352, 0x0357,
|
|
0x035B, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369,
|
|
0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F, 0x0483, 0x0484,
|
|
0x0485, 0x0486, 0x0487, 0x0592, 0x0593, 0x0594, 0x0595, 0x0597,
|
|
0x0598, 0x0599, 0x059C, 0x059D, 0x059E, 0x059F, 0x05A0, 0x05A1,
|
|
0x05A8, 0x05A9, 0x05AB, 0x05AC, 0x05AF, 0x05C4, 0x0610, 0x0611,
|
|
0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617, 0x0657, 0x0658,
|
|
0x0659, 0x065A, 0x065B, 0x065D, 0x065E, 0x06D6, 0x06D7, 0x06D8,
|
|
0x06D9, 0x06DA, 0x06DB, 0x06DC, 0x06DF, 0x06E0, 0x06E1, 0x06E2,
|
|
0x06E4, 0x06E7, 0x06E8, 0x06EB, 0x06EC, 0x0730, 0x0732, 0x0733,
|
|
0x0735, 0x0736, 0x073A, 0x073D, 0x073F, 0x0740, 0x0741, 0x0743,
|
|
0x0745, 0x0747, 0x0749, 0x074A, 0x07EB, 0x07EC, 0x07ED, 0x07EE,
|
|
0x07EF, 0x07F0, 0x07F1, 0x07F3, 0x0816, 0x0817, 0x0818, 0x0819,
|
|
0x081B, 0x081C, 0x081D, 0x081E, 0x081F, 0x0820, 0x0821, 0x0822,
|
|
0x0823, 0x0825, 0x0826, 0x0827, 0x0829, 0x082A, 0x082B, 0x082C,
|
|
0x082D, 0x0951, 0x0953, 0x0954, 0x0F82, 0x0F83, 0x0F86, 0x0F87,
|
|
0x135D, 0x135E, 0x135F, 0x17DD, 0x193A, 0x1A17, 0x1A75, 0x1A76,
|
|
0x1A77, 0x1A78, 0x1A79, 0x1A7A, 0x1A7B, 0x1A7C, 0x1B6B, 0x1B6D,
|
|
0x1B6E, 0x1B6F, 0x1B70, 0x1B71, 0x1B72, 0x1B73, 0x1CD0, 0x1CD1,
|
|
0x1CD2, 0x1CDA, 0x1CDB, 0x1CE0, 0x1DC0, 0x1DC1, 0x1DC3, 0x1DC4,
|
|
0x1DC5, 0x1DC6, 0x1DC7, 0x1DC8, 0x1DC9, 0x1DCB, 0x1DCC, 0x1DD1,
|
|
0x1DD2, 0x1DD3, 0x1DD4, 0x1DD5, 0x1DD6, 0x1DD7, 0x1DD8, 0x1DD9,
|
|
0x1DDA, 0x1DDB, 0x1DDC, 0x1DDD, 0x1DDE, 0x1DDF, 0x1DE0, 0x1DE1,
|
|
0x1DE2, 0x1DE3, 0x1DE4, 0x1DE5, 0x1DE6, 0x1DFE, 0x20D0, 0x20D1,
|
|
0x20D4, 0x20D5, 0x20D6, 0x20D7, 0x20DB, 0x20DC, 0x20E1, 0x20E7,
|
|
0x20E9, 0x20F0, 0x2CEF, 0x2CF0, 0x2CF1, 0x2DE0, 0x2DE1, 0x2DE2,
|
|
0x2DE3, 0x2DE4, 0x2DE5, 0x2DE6, 0x2DE7, 0x2DE8, 0x2DE9, 0x2DEA,
|
|
0x2DEB, 0x2DEC, 0x2DED, 0x2DEE, 0x2DEF, 0x2DF0, 0x2DF1, 0x2DF2,
|
|
0x2DF3, 0x2DF4, 0x2DF5, 0x2DF6, 0x2DF7, 0x2DF8, 0x2DF9, 0x2DFA,
|
|
0x2DFB, 0x2DFC, 0x2DFD, 0x2DFE, 0x2DFF, 0xA66F, 0xA67C, 0xA67D,
|
|
0xA6F0, 0xA6F1, 0xA8E0, 0xA8E1, 0xA8E2, 0xA8E3, 0xA8E4, 0xA8E5,
|
|
0xA8E6, 0xA8E7, 0xA8E8, 0xA8E9, 0xA8EA, 0xA8EB, 0xA8EC, 0xA8ED,
|
|
0xA8EE, 0xA8EF, 0xA8F0, 0xA8F1, 0xAAB0, 0xAAB2, 0xAAB3, 0xAAB7,
|
|
0xAAB8, 0xAABE, 0xAABF, 0xAAC1, 0xFE20, 0xFE21, 0xFE22, 0xFE23,
|
|
0xFE24, 0xFE25, 0xFE26, 0x10A0F, 0x10A38, 0x1D185, 0x1D186, 0x1D187,
|
|
0x1D188, 0x1D189, 0x1D1AA, 0x1D1AB, 0x1D1AC, 0x1D1AD, 0x1D242, 0x1D243,
|
|
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; /* Source pixel dimensions. */
|
|
u_int height;
|
|
u_int source_x; /* Crop origin in source pixels. */
|
|
u_int source_y;
|
|
u_int x_offset; /* Pixel offset inside the starting cell. */
|
|
u_int y_offset;
|
|
u_int source_width; /* Crop dimensions, zero for the remainder. */
|
|
u_int source_height;
|
|
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; /* 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; /* 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; /* 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; /* 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; /* 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;
|
|
/* Source creation order, independent of terminal upload order. */
|
|
u_int order_id;
|
|
u_int xpixel;
|
|
u_int ypixel;
|
|
struct kitty_placement_cache *placements;
|
|
u_int next_placement;
|
|
struct kitty_image_cache *next;
|
|
};
|
|
|
|
/* One terminal placement, retained until its redraw replacement is ready. */
|
|
struct kitty_placement_cache {
|
|
u_int id; /* Terminal placement ID. */
|
|
u_int x; /* Origin in terminal cells. */
|
|
u_int y;
|
|
u_int width; /* Size in cells. */
|
|
u_int height;
|
|
u_int source_x;
|
|
u_int source_y;
|
|
int64_t z; /* Logical layer. */
|
|
int32_t draw_z; /* Terminal layer. */
|
|
/* Delete after replacement placements have been drawn. */
|
|
int pending_delete;
|
|
struct kitty_placement_cache *next;
|
|
};
|
|
|
|
/* Cached placements sorted temporarily when their output layers change. */
|
|
struct kitty_layer {
|
|
struct kitty_image_cache *entry;
|
|
struct kitty_placement_cache *placement;
|
|
};
|
|
|
|
enum kitty_layer_band {
|
|
KITTY_LAYER_NONE,
|
|
KITTY_LAYER_BELOW_BACKGROUND,
|
|
KITTY_LAYER_BELOW_TEXT,
|
|
KITTY_LAYER_SIXEL,
|
|
KITTY_LAYER_ABOVE_TEXT
|
|
};
|
|
|
|
/* Image uploads and ID allocation kept for the lifetime of a terminal. */
|
|
struct kitty_output {
|
|
struct kitty_image_cache *images;
|
|
u_int next_id;
|
|
int layers_changed;
|
|
};
|
|
|
|
/* Return the Kitty output state for a terminal. */
|
|
static struct kitty_output *
|
|
kitty_get_output(struct tty *tty)
|
|
{
|
|
struct kitty_output *ko = tty->image_data;
|
|
|
|
if (ko == NULL) {
|
|
ko = xcalloc(1, sizeof *ko);
|
|
ko->next_id = arc4random_uniform(0xffff00) + 1;
|
|
tty->image_data = ko;
|
|
}
|
|
return (ko);
|
|
}
|
|
|
|
/* Delete a Kitty image and its placements. */
|
|
static void
|
|
kitty_delete(struct tty *tty, u_int id)
|
|
{
|
|
char s[64];
|
|
|
|
xsnprintf(s, sizeof s, "\033_Ga=d,d=I,i=%u,q=2\033\\", id);
|
|
tty_puts(tty, s);
|
|
}
|
|
|
|
/* Free cached output placement records. */
|
|
static void
|
|
kitty_free_placements(struct kitty_image_cache *entry)
|
|
{
|
|
struct kitty_placement_cache *placement, *next;
|
|
|
|
for (placement = entry->placements; placement != NULL;
|
|
placement = next) {
|
|
next = placement->next;
|
|
free(placement);
|
|
}
|
|
entry->placements = NULL;
|
|
}
|
|
|
|
static void kitty_place(struct tty *, struct kitty_image_cache *,
|
|
struct image *, u_int, u_int, u_int, u_int, u_int, u_int,
|
|
int64_t);
|
|
static void kitty_send_placement(struct tty *, struct kitty_image_cache *,
|
|
struct image *, struct kitty_placement_cache *);
|
|
|
|
/* Compare output placements by logical z-index and source creation order. */
|
|
static int
|
|
kitty_layer_cmp(const void *left, const void *right)
|
|
{
|
|
const struct kitty_layer *a = left, *b = right;
|
|
|
|
if (a->placement->z < b->placement->z)
|
|
return (-1);
|
|
if (a->placement->z > b->placement->z)
|
|
return (1);
|
|
if (a->entry->order_id < b->entry->order_id)
|
|
return (-1);
|
|
if (a->entry->order_id > b->entry->order_id)
|
|
return (1);
|
|
return (0);
|
|
}
|
|
|
|
/* Return the output band relative to cell backgrounds, text and SIXEL. */
|
|
static enum kitty_layer_band
|
|
kitty_layer_band(int64_t z)
|
|
{
|
|
if (z < INT32_MIN / 2)
|
|
return (KITTY_LAYER_BELOW_BACKGROUND);
|
|
if (z < 0)
|
|
return (KITTY_LAYER_BELOW_TEXT);
|
|
if (z == 0)
|
|
return (KITTY_LAYER_SIXEL);
|
|
return (KITTY_LAYER_ABOVE_TEXT);
|
|
}
|
|
|
|
/* Assign terminal layers without relying on the order images were uploaded. */
|
|
static void
|
|
kitty_update_layers(struct tty *tty, struct kitty_output *ko)
|
|
{
|
|
struct kitty_image_cache *entry;
|
|
struct kitty_placement_cache *placement;
|
|
struct kitty_layer *layers;
|
|
struct image *im;
|
|
size_t count = 0, i;
|
|
enum kitty_layer_band band;
|
|
enum kitty_layer_band previous_band = KITTY_LAYER_NONE;
|
|
int32_t draw_z = 0;
|
|
|
|
/* Gather surviving placements, including pieces outside the redraw. */
|
|
for (entry = ko->images; entry != NULL; entry = entry->next) {
|
|
for (placement = entry->placements; placement != NULL;
|
|
placement = placement->next)
|
|
count++;
|
|
}
|
|
if (count == 0)
|
|
return;
|
|
layers = xcalloc(count, sizeof *layers);
|
|
i = 0;
|
|
for (entry = ko->images; entry != NULL; entry = entry->next) {
|
|
for (placement = entry->placements; placement != NULL;
|
|
placement = placement->next) {
|
|
layers[i].entry = entry;
|
|
layers[i++].placement = placement;
|
|
}
|
|
}
|
|
qsort(layers, count, sizeof *layers, kitty_layer_cmp);
|
|
|
|
/* Keep each band on the same side of background cells and text. */
|
|
for (i = 0; i < count; i++) {
|
|
entry = layers[i].entry;
|
|
placement = layers[i].placement;
|
|
band = kitty_layer_band(placement->z);
|
|
if (band != previous_band) {
|
|
switch (band) {
|
|
case KITTY_LAYER_BELOW_BACKGROUND:
|
|
draw_z = INT32_MIN;
|
|
break;
|
|
case KITTY_LAYER_BELOW_TEXT:
|
|
draw_z = INT32_MIN / 2;
|
|
break;
|
|
case KITTY_LAYER_SIXEL:
|
|
draw_z = 0;
|
|
break;
|
|
default:
|
|
draw_z = 1;
|
|
break;
|
|
}
|
|
previous_band = band;
|
|
} else if (band != KITTY_LAYER_SIXEL &&
|
|
(placement->z != layers[i - 1].placement->z ||
|
|
entry->order_id != layers[i - 1].entry->order_id)) {
|
|
draw_z++;
|
|
}
|
|
if (placement->draw_z == draw_z)
|
|
continue;
|
|
placement->draw_z = draw_z;
|
|
im = image_find(entry->server_id);
|
|
if (im != NULL)
|
|
kitty_send_placement(tty, entry, im, placement);
|
|
}
|
|
free(layers);
|
|
}
|
|
|
|
/* Place one piece of an existing placement again as a new placement. */
|
|
static void
|
|
kitty_redraw_keep_piece(struct tty *tty, struct kitty_image_cache *entry,
|
|
struct image *im, struct kitty_placement_cache *placement, u_int x,
|
|
u_int y, u_int width, u_int height)
|
|
{
|
|
if (width == 0 || height == 0)
|
|
return;
|
|
kitty_place(tty, entry, im, placement->source_x + (x - placement->x),
|
|
placement->source_y + (y - placement->y), width, height, x, y,
|
|
placement->z);
|
|
}
|
|
|
|
/* 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,
|
|
u_int height)
|
|
{
|
|
struct image *im;
|
|
u_int px0, px1, py0, py1, ix0, ix1, iy0, iy1;
|
|
|
|
px0 = placement->x;
|
|
px1 = placement->x + placement->width;
|
|
py0 = placement->y;
|
|
py1 = placement->y + placement->height;
|
|
ix0 = (x > px0 ? x : px0);
|
|
ix1 = (x + width < px1 ? x + width : px1);
|
|
iy0 = (y > py0 ? y : py0);
|
|
iy1 = (y + height < py1 ? y + height : py1);
|
|
if (ix0 == px0 && ix1 == px1 && iy0 == py0 && iy1 == py1)
|
|
return;
|
|
|
|
im = image_find(entry->server_id);
|
|
if (im == NULL)
|
|
return;
|
|
|
|
/* Callers have checked the areas intersect, so ix0 < ix1, iy0 < iy1. */
|
|
kitty_redraw_keep_piece(tty, entry, im, placement, px0, py0,
|
|
px1 - px0, iy0 - py0);
|
|
kitty_redraw_keep_piece(tty, entry, im, placement, px0, iy1,
|
|
px1 - px0, py1 - iy1);
|
|
kitty_redraw_keep_piece(tty, entry, im, placement, px0, iy0,
|
|
ix0 - px0, iy1 - iy0);
|
|
kitty_redraw_keep_piece(tty, entry, im, placement, ix1, iy0,
|
|
px1 - ix1, iy1 - iy0);
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
struct kitty_output *ko = tty->image_data;
|
|
struct kitty_image_cache *entry;
|
|
struct kitty_placement_cache *placement;
|
|
|
|
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) {
|
|
if (placement->pending_delete)
|
|
continue;
|
|
if (placement->x >= x + width ||
|
|
placement->x + placement->width <= x ||
|
|
placement->y >= y + height ||
|
|
placement->y + placement->height <= y) {
|
|
continue;
|
|
}
|
|
placement->pending_delete = 1;
|
|
ko->layers_changed = 1;
|
|
|
|
/* Preserve pieces outside the redraw area. */
|
|
kitty_redraw_keep(tty, entry, placement, x, y, width,
|
|
height);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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];
|
|
|
|
if (ko == NULL)
|
|
return;
|
|
for (entry = ko->images; entry != NULL; entry = entry->next) {
|
|
for (pp = &entry->placements; (placement = *pp) != NULL; ) {
|
|
if (!placement->pending_delete) {
|
|
pp = &placement->next;
|
|
continue;
|
|
}
|
|
xsnprintf(s, sizeof s,
|
|
"\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);
|
|
}
|
|
}
|
|
if (ko->layers_changed) {
|
|
kitty_update_layers(tty, ko);
|
|
ko->layers_changed = 0;
|
|
}
|
|
}
|
|
|
|
/* Free a cached Kitty image. */
|
|
static void
|
|
kitty_free_entry(struct tty *tty, struct kitty_image_cache *entry, int send)
|
|
{
|
|
if (send)
|
|
kitty_delete(tty, entry->kitty_id);
|
|
kitty_free_placements(entry);
|
|
free(entry);
|
|
}
|
|
|
|
/* Free Kitty output state for a terminal. */
|
|
void
|
|
kitty_free_output_state(struct tty *tty, int send)
|
|
{
|
|
struct kitty_output *ko = tty->image_data;
|
|
struct kitty_image_cache *entry, *next;
|
|
|
|
if (ko == NULL)
|
|
return;
|
|
/* Free all cached image entries. */
|
|
for (entry = ko->images; entry != NULL; entry = next) {
|
|
next = entry->next;
|
|
kitty_free_entry(tty, entry, send);
|
|
}
|
|
free(ko);
|
|
tty->image_data = NULL;
|
|
}
|
|
|
|
/* Remove cached Kitty images no longer held by the server. */
|
|
static void
|
|
kitty_free_stale_images(struct tty *tty)
|
|
{
|
|
struct kitty_output *ko = tty->image_data;
|
|
struct kitty_image_cache *entry, *next, *previous;
|
|
|
|
if (ko == NULL)
|
|
return;
|
|
previous = NULL;
|
|
for (entry = ko->images; entry != NULL; entry = next) {
|
|
/* Save the successor before this entry may be removed. */
|
|
next = entry->next;
|
|
if (image_find(entry->server_id) != NULL) {
|
|
/* Retain this entry as the next one's predecessor. */
|
|
previous = entry;
|
|
continue;
|
|
}
|
|
|
|
/* Unlink stale entries, including the list head. */
|
|
if (previous == NULL)
|
|
ko->images = next;
|
|
else
|
|
previous->next = next;
|
|
kitty_free_entry(tty, entry, 1);
|
|
ko->layers_changed = 1;
|
|
}
|
|
}
|
|
|
|
/* Send the source rectangle and output layer for a cached placement. */
|
|
static void
|
|
kitty_send_placement(struct tty *tty, struct kitty_image_cache *entry,
|
|
struct image *im, struct kitty_placement_cache *placement)
|
|
{
|
|
char control[192];
|
|
u_int px, py, pwidth, pheight, sx, sy;
|
|
u_int canvas_width, canvas_height;
|
|
|
|
image_get_size_in_cells(im, &sx, &sy);
|
|
image_get_canvas_size(im, &canvas_width, &canvas_height);
|
|
px = (uint64_t)placement->source_x * canvas_width / sx;
|
|
py = (uint64_t)placement->source_y * canvas_height / sy;
|
|
pwidth = ((uint64_t)(placement->source_x + placement->width) *
|
|
canvas_width + sx - 1) / sx - px;
|
|
pheight = ((uint64_t)(placement->source_y + placement->height) *
|
|
canvas_height + sy - 1) / sy - py;
|
|
|
|
/* Account for the duplicate-pixel border added by kitty_upload(). */
|
|
px++;
|
|
py++;
|
|
tty_cursor(tty, placement->x, placement->y);
|
|
xsnprintf(control, sizeof control,
|
|
"\033_Ga=p,i=%u,p=%u,x=%u,y=%u,w=%u,h=%u,c=%u,r=%u,z=%d,"
|
|
"C=1,q=2\033\\", entry->kitty_id,
|
|
placement->id, px, py, pwidth, pheight, placement->width,
|
|
placement->height, placement->draw_z);
|
|
tty_puts(tty, control);
|
|
}
|
|
|
|
/* Place an image rectangle using the Kitty graphics protocol. */
|
|
static void
|
|
kitty_place(struct tty *tty, struct kitty_image_cache *entry,
|
|
struct image *im, u_int source_x, u_int source_y, u_int width,
|
|
u_int height, u_int destination_x, u_int destination_y, int64_t z)
|
|
{
|
|
struct kitty_placement_cache *placement;
|
|
|
|
placement = xcalloc(1, sizeof *placement);
|
|
do {
|
|
placement->id = ++entry->next_placement;
|
|
} while (placement->id == 0);
|
|
|
|
placement->x = destination_x;
|
|
placement->y = destination_y;
|
|
placement->width = width;
|
|
placement->height = height;
|
|
placement->source_x = source_x;
|
|
placement->source_y = source_y;
|
|
placement->z = z;
|
|
if (z > INT32_MAX)
|
|
placement->draw_z = INT32_MAX;
|
|
else
|
|
placement->draw_z = z;
|
|
placement->next = entry->placements;
|
|
entry->placements = placement;
|
|
kitty_get_output(tty)->layers_changed = 1;
|
|
kitty_send_placement(tty, entry, im, placement);
|
|
}
|
|
|
|
/* Upload an image to Kitty and return its output cache entry. */
|
|
static struct kitty_image_cache *
|
|
kitty_upload(struct tty *tty, struct image *im)
|
|
{
|
|
struct kitty_output *ko = kitty_get_output(tty);
|
|
struct kitty_image_cache *entry;
|
|
char control[128], encoded[4097];
|
|
const u_char *pixels;
|
|
u_char *padded, *padded_row;
|
|
size_t offset, size, row, stride, image_size;
|
|
int encodedlen;
|
|
u_int id, width, height;
|
|
u_int canvas_width, canvas_height;
|
|
u_int upload_width, upload_height;
|
|
|
|
image_get_size(im, &width, &height);
|
|
image_get_canvas_size(im, &canvas_width, &canvas_height);
|
|
if (canvas_width > UINT_MAX - 2 || canvas_height > UINT_MAX - 2)
|
|
return (NULL);
|
|
upload_width = canvas_width + 2;
|
|
upload_height = canvas_height + 2;
|
|
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) {
|
|
return (entry);
|
|
}
|
|
kitty_delete(tty, entry->kitty_id);
|
|
kitty_free_placements(entry);
|
|
entry->server_id = 0;
|
|
break;
|
|
}
|
|
if (entry == NULL) {
|
|
entry = xcalloc(1, sizeof *entry);
|
|
entry->next = ko->images;
|
|
ko->images = entry;
|
|
}
|
|
|
|
/* Allocate a nonzero terminal image ID. */
|
|
do {
|
|
id = ++ko->next_id & 0xffffff;
|
|
} while (id == 0);
|
|
entry->server_id = image_get_id(im);
|
|
entry->kitty_id = id;
|
|
entry->order_id = image_get_order_id(im);
|
|
entry->xpixel = tty->xpixel;
|
|
entry->ypixel = tty->ypixel;
|
|
entry->next_placement = 0;
|
|
|
|
pixels = image_get_pixels(im, &stride, &image_size);
|
|
|
|
/* 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 + ((row + 1) * upload_width + 1) * 4,
|
|
pixels + row * stride, (size_t)width * 4);
|
|
}
|
|
for (row = 1; row <= canvas_height; row++) {
|
|
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);
|
|
memcpy(padded + (size_t)(upload_height - 1) * upload_width * 4,
|
|
padded + (size_t)(upload_height - 2) * upload_width * 4,
|
|
(size_t)upload_width * 4);
|
|
pixels = padded;
|
|
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)
|
|
size = KITTY_CHUNK_SIZE;
|
|
encodedlen = b64_ntop(pixels + offset, size, encoded,
|
|
sizeof encoded);
|
|
if (encodedlen < 0) {
|
|
free(padded);
|
|
return (NULL);
|
|
}
|
|
if (offset == 0) {
|
|
xsnprintf(control, sizeof control,
|
|
"\033_Ga=t,f=32,s=%u,v=%u,i=%u,q=2,m=%d;",
|
|
width, height, id, offset + size < image_size);
|
|
} else {
|
|
xsnprintf(control, sizeof control, "\033_Gm=%d;",
|
|
offset + size < image_size);
|
|
}
|
|
tty_puts(tty, control);
|
|
tty_putn(tty, encoded, encodedlen, 0);
|
|
tty_puts(tty, "\033\\");
|
|
}
|
|
free(padded);
|
|
return (entry);
|
|
}
|
|
|
|
/* Draw an image rectangle using the Kitty graphics protocol. */
|
|
void
|
|
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;
|
|
u_int destination_x, destination_y;
|
|
int64_t z;
|
|
|
|
im = image_rect_get_image(rectangle);
|
|
kitty_free_stale_images(tty);
|
|
entry = kitty_upload(tty, im);
|
|
if (entry == NULL)
|
|
return;
|
|
image_rect_get_coords(rectangle, &source_x, &source_y, &width,
|
|
&height, &destination_x, &destination_y);
|
|
z = image_rect_get_z(rectangle);
|
|
kitty_place(tty, entry, im, source_x, source_y, width, height,
|
|
destination_x, destination_y, z);
|
|
}
|
|
|
|
/* Parse an unsigned Kitty graphics control value. */
|
|
static int
|
|
kitty_number(const char *s, size_t len, u_int *value)
|
|
{
|
|
char copy[32];
|
|
const char *errstr;
|
|
long long ll;
|
|
|
|
if (len == 0 || len >= sizeof copy)
|
|
return (-1);
|
|
memcpy(copy, s, len);
|
|
copy[len] = '\0';
|
|
ll = strtonum(copy, 0, UINT_MAX, &errstr);
|
|
if (errstr != NULL)
|
|
return (-1);
|
|
*value = ll;
|
|
return (0);
|
|
}
|
|
|
|
/* Parse a signed Kitty graphics control value. */
|
|
static int
|
|
kitty_signed_number(const char *s, size_t len, int32_t *value)
|
|
{
|
|
char copy[32];
|
|
const char *errstr;
|
|
long long ll;
|
|
|
|
if (len == 0 || len >= sizeof copy)
|
|
return (-1);
|
|
memcpy(copy, s, len);
|
|
copy[len] = '\0';
|
|
ll = strtonum(copy, INT32_MIN, INT32_MAX, &errstr);
|
|
if (errstr != NULL)
|
|
return (-1);
|
|
*value = ll;
|
|
return (0);
|
|
}
|
|
|
|
/* Parse a Kitty graphics control string. */
|
|
static int
|
|
kitty_control(struct kitty_state *ks, const u_char *buf, size_t len)
|
|
{
|
|
const u_char *value, *end = buf + len, *comma;
|
|
size_t valuelen;
|
|
u_int number;
|
|
int32_t signed_number;
|
|
char key;
|
|
|
|
/* Read comma-separated keys and validate each control value. */
|
|
while (buf < end) {
|
|
key = *buf++;
|
|
if (buf == end || *buf++ != '=')
|
|
return (-1);
|
|
value = buf;
|
|
comma = memchr(buf, ',', end - buf);
|
|
if (comma == NULL) {
|
|
valuelen = end - buf;
|
|
buf = end;
|
|
} else {
|
|
valuelen = comma - buf;
|
|
buf = comma + 1;
|
|
}
|
|
if (valuelen == 0)
|
|
return (-1);
|
|
|
|
switch (key) {
|
|
case 'a':
|
|
ks->action = value[0];
|
|
break;
|
|
case 'd':
|
|
ks->delete = value[0];
|
|
break;
|
|
case 't':
|
|
ks->medium = value[0];
|
|
break;
|
|
case 'o':
|
|
ks->compression = value[0];
|
|
break;
|
|
case 'P': case 'Q':
|
|
if (kitty_number((const char *)value, valuelen,
|
|
&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) {
|
|
return (-1);
|
|
}
|
|
if (signed_number != 0)
|
|
ks->unsupported = 1;
|
|
break;
|
|
case 'z':
|
|
if (kitty_signed_number((const char *)value, valuelen,
|
|
&ks->z) != 0) {
|
|
return (-1);
|
|
}
|
|
break;
|
|
case 'f':
|
|
case 's':
|
|
case 'v':
|
|
case 'x':
|
|
case 'y':
|
|
case 'w':
|
|
case 'h':
|
|
case 'c':
|
|
case 'r':
|
|
case 'i':
|
|
case 'I':
|
|
case 'p':
|
|
case 'q':
|
|
case 'm':
|
|
case 'S':
|
|
case 'C':
|
|
case 'U':
|
|
case 'X':
|
|
case 'Y':
|
|
if (kitty_number((const char *)value, valuelen,
|
|
&number) != 0) {
|
|
return (-1);
|
|
}
|
|
switch (key) {
|
|
case 'f': ks->format = number; break;
|
|
case 's': ks->width = number; break;
|
|
case 'v': ks->height = number; break;
|
|
case 'x': ks->source_x = number; break;
|
|
case 'y': ks->source_y = number; break;
|
|
case 'w': ks->source_width = number; break;
|
|
case 'h': ks->source_height = number; break;
|
|
case 'c': ks->columns = number; break;
|
|
case 'r': ks->rows = number; break;
|
|
case 'i': ks->image_id = number; break;
|
|
case 'I': ks->image_number = number; break;
|
|
case 'p': ks->placement_id = number; break;
|
|
case 'q': ks->quiet = number; break;
|
|
case 'm': ks->more = (number != 0); break;
|
|
case 'S': ks->data_size = number; break;
|
|
case 'C': ks->no_cursor = (number != 0); break;
|
|
case 'U': ks->virtual = (number != 0); break;
|
|
case 'X': ks->x_offset = number; break;
|
|
case 'Y': ks->y_offset = number; break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
/* Free a partially parsed Kitty graphics command. */
|
|
static void
|
|
kitty_state_free(struct kitty_state *ks)
|
|
{
|
|
if (ks == NULL)
|
|
return;
|
|
free(ks->raw);
|
|
free(ks);
|
|
}
|
|
|
|
/* Free placement images belonging to a Kitty source image. */
|
|
static void
|
|
kitty_placements_free(struct kitty_source *source)
|
|
{
|
|
struct kitty_placement *placement, *next;
|
|
|
|
for (placement = source->placements; placement != NULL;
|
|
placement = next) {
|
|
next = placement->next;
|
|
image_free(image_find(placement->server_id));
|
|
free(placement);
|
|
}
|
|
source->placements = NULL;
|
|
}
|
|
|
|
/* Free a source image and its parser-side placements. */
|
|
static void
|
|
kitty_source_free(struct kitty_source *source)
|
|
{
|
|
kitty_placements_free(source);
|
|
image_free(image_find(source->server_id));
|
|
free(source);
|
|
}
|
|
|
|
/* Free Kitty graphics parser state. */
|
|
void
|
|
kitty_free_state(void *state)
|
|
{
|
|
struct kitty_context *kc = state;
|
|
struct kitty_source *source, *next;
|
|
|
|
if (kc == NULL)
|
|
return;
|
|
kitty_state_free(kc->transfer);
|
|
for (source = kc->sources; source != NULL; source = next) {
|
|
next = source->next;
|
|
kitty_source_free(source);
|
|
}
|
|
free(kc);
|
|
}
|
|
|
|
/* Find a Kitty source image by application ID. */
|
|
static struct kitty_source *
|
|
kitty_source_find(struct kitty_context *kc, u_int id)
|
|
{
|
|
struct kitty_source *source;
|
|
|
|
for (source = kc->sources; source != NULL; source = source->next) {
|
|
if (source->app_id == id)
|
|
return (source);
|
|
}
|
|
return (NULL);
|
|
}
|
|
|
|
/* Replace the source image associated with a Kitty application ID. */
|
|
static u_int
|
|
kitty_source_set(struct kitty_context *kc, u_int id, struct image *im)
|
|
{
|
|
struct kitty_source *source;
|
|
struct image *old;
|
|
u_int old_id = 0;
|
|
|
|
if (id == 0)
|
|
return (0);
|
|
source = kitty_source_find(kc, id);
|
|
if (source == NULL) {
|
|
source = xcalloc(1, sizeof *source);
|
|
source->app_id = id;
|
|
source->next = kc->sources;
|
|
kc->sources = source;
|
|
} else {
|
|
old_id = source->server_id;
|
|
old = image_find(old_id);
|
|
if (old != NULL)
|
|
image_set_order_id(im, image_get_order_id(old));
|
|
kitty_placements_free(source);
|
|
image_free(image_find(source->server_id));
|
|
}
|
|
image_ref(im);
|
|
source->server_id = image_get_id(im);
|
|
return (old_id);
|
|
}
|
|
|
|
/* Associate a placement ID with an image. */
|
|
static u_int
|
|
kitty_placement_set(struct kitty_context *kc, const struct kitty_state *ks,
|
|
struct image *im)
|
|
{
|
|
struct kitty_source *source;
|
|
struct kitty_placement *placement;
|
|
u_int old_id;
|
|
|
|
source = kitty_source_find(kc, ks->image_id);
|
|
if (source == NULL)
|
|
return (0);
|
|
for (placement = source->placements; placement != NULL;
|
|
placement = placement->next) {
|
|
if (ks->placement_id != 0) {
|
|
if (placement->placement_id == ks->placement_id)
|
|
break;
|
|
}
|
|
}
|
|
if (placement == NULL) {
|
|
placement = xcalloc(1, sizeof *placement);
|
|
placement->placement_id = ks->placement_id;
|
|
placement->next = source->placements;
|
|
source->placements = placement;
|
|
}
|
|
old_id = placement->server_id;
|
|
placement->z = ks->z;
|
|
placement->virtual = ks->virtual;
|
|
image_ref(im);
|
|
placement->server_id = image_get_id(im);
|
|
if (old_id != 0)
|
|
image_free(image_find(old_id));
|
|
return (old_id);
|
|
}
|
|
|
|
/* Find and reference a Kitty source image. */
|
|
static struct image *
|
|
kitty_source_get(struct kitty_context *kc, u_int id)
|
|
{
|
|
struct kitty_source *source;
|
|
struct image *im;
|
|
|
|
source = kitty_source_find(kc, id);
|
|
if (source == NULL)
|
|
return (NULL);
|
|
im = image_find(source->server_id);
|
|
if (im != NULL)
|
|
image_ref(im);
|
|
return (im);
|
|
}
|
|
|
|
/* Remove records for ordinary placements no longer present in the grid. */
|
|
static u_int
|
|
kitty_prune_placements(struct kitty_source *source, struct grid *gd)
|
|
{
|
|
struct kitty_placement **pp, *placement;
|
|
u_int removed = 0;
|
|
|
|
for (pp = &source->placements; (placement = *pp) != NULL; ) {
|
|
if (placement->virtual)
|
|
goto keep;
|
|
if (image_grid_has_image(gd, placement->server_id))
|
|
goto keep;
|
|
*pp = placement->next;
|
|
image_free(image_find(placement->server_id));
|
|
free(placement);
|
|
removed++;
|
|
continue;
|
|
keep:
|
|
pp = &placement->next;
|
|
}
|
|
return (removed);
|
|
}
|
|
|
|
/* Delete selected placements and release unreferenced image data. */
|
|
void
|
|
kitty_delete_images(void *state, struct screen_write_ctx *ctx,
|
|
const struct kitty_parse_result *result)
|
|
{
|
|
struct kitty_context *kc = state;
|
|
struct grid *gd = ctx->s->grid;
|
|
struct kitty_source **sp, *source;
|
|
struct kitty_placement **pp, *placement;
|
|
u_int removed;
|
|
u_int placement_id = result->placement_id;
|
|
char how = result->delete;
|
|
int selected, release = 0;
|
|
|
|
if (kc == NULL)
|
|
return;
|
|
|
|
/* 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);
|
|
for (sp = &kc->sources; (source = *sp) != NULL; ) {
|
|
removed = kitty_prune_placements(source, gd);
|
|
selected = 0;
|
|
if (how == 'i') {
|
|
if (source->app_id == result->image_id)
|
|
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 &&
|
|
placement->placement_id != placement_id) {
|
|
goto keep_placement;
|
|
}
|
|
*pp = placement->next;
|
|
image_free(image_find(placement->server_id));
|
|
free(placement);
|
|
removed++;
|
|
continue;
|
|
keep_placement:
|
|
pp = &placement->next;
|
|
}
|
|
if (placement_id == 0)
|
|
removed++;
|
|
}
|
|
|
|
/* 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;
|
|
*sp = source->next;
|
|
kitty_source_free(source);
|
|
continue;
|
|
keep_source:
|
|
sp = &source->next;
|
|
}
|
|
}
|
|
|
|
/* Decode and append one base64-encoded Kitty payload chunk. */
|
|
static int
|
|
kitty_append(struct kitty_state *ks, const u_char *buf, size_t len)
|
|
{
|
|
static const char base64[] =
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
u_char *decoded;
|
|
char *copy = NULL;
|
|
const char *digit;
|
|
size_t decodedlen, offset;
|
|
u_int mask;
|
|
|
|
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;
|
|
mask = 0x3c;
|
|
if (buf[offset] == '=') {
|
|
offset--;
|
|
mask = 0x30;
|
|
}
|
|
digit = strchr(base64, buf[offset]);
|
|
if (digit == NULL)
|
|
return (-1);
|
|
copy = xmalloc(len);
|
|
memcpy(copy, buf, len);
|
|
copy[offset] = base64[(digit - base64) & mask];
|
|
buf = (const u_char *)copy;
|
|
}
|
|
|
|
decoded = image_base64_decode((const char *)buf, len,
|
|
IMAGE_SIZE_LIMIT, &decodedlen);
|
|
free(copy);
|
|
if (decoded == NULL)
|
|
return (-1);
|
|
|
|
/* Empty decoded chunks need no allocation. */
|
|
if (decodedlen == 0) {
|
|
free(decoded);
|
|
return (0);
|
|
}
|
|
if (decodedlen > IMAGE_SIZE_LIMIT - ks->rawlen) {
|
|
free(decoded);
|
|
return (-1);
|
|
}
|
|
ks->raw = xrealloc(ks->raw, ks->rawlen + decodedlen);
|
|
memcpy(ks->raw + ks->rawlen, decoded, decodedlen);
|
|
ks->rawlen += decodedlen;
|
|
free(decoded);
|
|
return (0);
|
|
}
|
|
|
|
/* Decode raw Kitty graphics data into RGBA pixels. */
|
|
static u_char *
|
|
kitty_raw(struct kitty_state *ks, u_char *data, size_t size)
|
|
{
|
|
u_char *raw, *pixels;
|
|
size_t expected, i, j;
|
|
uLongf rawlen;
|
|
u_int bytes;
|
|
|
|
bytes = (ks->format == 24 ? 3 : 4);
|
|
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);
|
|
if (uncompress(raw, &rawlen, data, size) != Z_OK ||
|
|
rawlen != expected) {
|
|
free(raw);
|
|
return (NULL);
|
|
}
|
|
size = rawlen;
|
|
} else if (ks->compression != '\0')
|
|
return (NULL);
|
|
if (size != expected) {
|
|
if (raw != data)
|
|
free(raw);
|
|
return (NULL);
|
|
}
|
|
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];
|
|
pixels[j + 1] = raw[i + 1];
|
|
pixels[j + 2] = raw[i + 2];
|
|
pixels[j + 3] = 255;
|
|
}
|
|
if (raw != data)
|
|
free(raw);
|
|
return (pixels);
|
|
}
|
|
|
|
/* Create an image view for a Kitty placement. */
|
|
static struct image *
|
|
kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel,
|
|
u_int ypixel)
|
|
{
|
|
struct image_view view = { 0 };
|
|
struct image *im;
|
|
u_int cell_width, cell_height, source_width, source_height;
|
|
u_int x_offset, y_offset;
|
|
uint64_t canvas_width, canvas_height;
|
|
double width_scale = 1, height_scale = 1;
|
|
double scale = 1, units = 1;
|
|
double value;
|
|
int natural_size = 0;
|
|
int stretch;
|
|
|
|
stretch = !ks->virtual && ks->columns != 0 && ks->rows != 0;
|
|
image_get_size(source, &source_width, &source_height);
|
|
view.x = ks->source_x;
|
|
view.y = ks->source_y;
|
|
|
|
/* 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 && view.width > ks->source_width)
|
|
view.width = ks->source_width;
|
|
view.height = source_height - view.y;
|
|
if (ks->source_height != 0 && view.height > ks->source_height)
|
|
view.height = ks->source_height;
|
|
|
|
/* Preserve aspect ratio when computing an automatic cell dimension. */
|
|
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;
|
|
if (y_offset >= cell_height)
|
|
y_offset = cell_height - 1;
|
|
view.sx = ks->columns;
|
|
view.sy = ks->rows;
|
|
if (view.sx > USHRT_MAX || view.sy > USHRT_MAX)
|
|
return (NULL);
|
|
if (view.sx == 0 && view.sy == 0)
|
|
natural_size = 1;
|
|
if (natural_size) {
|
|
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);
|
|
} else if (view.sy == 0) {
|
|
scale = ((double)view.sx * cell_width - x_offset) / view.width;
|
|
width_scale = height_scale = scale;
|
|
value = ceil((view.height * scale + y_offset) / cell_height);
|
|
if (value > USHRT_MAX)
|
|
return (NULL);
|
|
view.sy = value;
|
|
} else if (view.sx == 0) {
|
|
scale = ((double)view.sy * cell_height - y_offset) /
|
|
view.height;
|
|
width_scale = height_scale = scale;
|
|
value = ceil((view.width * scale + x_offset) / cell_width);
|
|
if (value > USHRT_MAX)
|
|
return (NULL);
|
|
view.sx = value;
|
|
} else {
|
|
width_scale = ((double)view.sx * cell_width - x_offset) /
|
|
view.width;
|
|
height_scale = ((double)view.sy * cell_height - y_offset) /
|
|
view.height;
|
|
scale = width_scale;
|
|
if (height_scale < scale)
|
|
scale = height_scale;
|
|
|
|
/* Only Unicode placeholders preserve aspect ratio here. */
|
|
if (ks->virtual)
|
|
width_scale = height_scale = scale;
|
|
}
|
|
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;
|
|
value = ceil(canvas_width * units);
|
|
if (value > UINT_MAX)
|
|
return (NULL);
|
|
view.canvas_width = value;
|
|
value = ceil(canvas_height * units);
|
|
if (value > UINT_MAX)
|
|
return (NULL);
|
|
view.canvas_height = value;
|
|
view.scaled_width = floor(view.width * width_scale * units);
|
|
if (view.scaled_width < view.width)
|
|
view.scaled_width = view.width;
|
|
view.scaled_height = floor(view.height * height_scale * units);
|
|
if (view.scaled_height < view.height)
|
|
view.scaled_height = view.height;
|
|
view.canvas_xoff = floor(x_offset * units);
|
|
view.canvas_yoff = floor(y_offset * units);
|
|
if (view.canvas_xoff > view.canvas_width ||
|
|
view.scaled_width > view.canvas_width - view.canvas_xoff ||
|
|
view.canvas_yoff > view.canvas_height ||
|
|
view.scaled_height > view.canvas_height - view.canvas_yoff) {
|
|
return (NULL);
|
|
}
|
|
|
|
/* Centre Unicode-placeholder images within their rectangle. */
|
|
if (ks->virtual && ks->columns != 0 && ks->rows != 0) {
|
|
view.canvas_xoff += (view.canvas_width - view.canvas_xoff -
|
|
view.scaled_width) / 2;
|
|
view.canvas_yoff += (view.canvas_height - view.canvas_yoff -
|
|
view.scaled_height) / 2;
|
|
}
|
|
/* Let the output terminal scale unpadded rectangular placements. */
|
|
if (x_offset == 0 && y_offset == 0 &&
|
|
(stretch ||
|
|
(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.canvas_xoff = 0;
|
|
view.canvas_yoff = 0;
|
|
}
|
|
im = image_create_view(source, &view);
|
|
if (im != NULL && ks->no_cursor)
|
|
image_set_no_cursor(im);
|
|
return (im);
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
struct kitty_context *kc = *state;
|
|
struct kitty_state *ks;
|
|
struct kitty_state command = { .action = 't' };
|
|
const u_char *semi;
|
|
u_char *decoded, *pixels;
|
|
u_char *uncompressed;
|
|
size_t controllen, payloadlen, decodedlen;
|
|
uLongf uncompressedlen;
|
|
u_int sx, sy, cell_width, cell_height;
|
|
uint64_t canvas_width, canvas_height;
|
|
struct image *im = NULL, *source;
|
|
int error;
|
|
|
|
if (kc == NULL) {
|
|
kc = xcalloc(1, sizeof *kc);
|
|
*state = kc;
|
|
}
|
|
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));
|
|
payloadlen = (semi == NULL ? 0 : len - controllen - 1);
|
|
if (ks != NULL) {
|
|
/* A delete command aborts an incomplete upload. */
|
|
(void)kitty_control(&command, buf, controllen);
|
|
if (command.action == 'd') {
|
|
kitty_state_free(ks);
|
|
kc->transfer = ks = NULL;
|
|
}
|
|
}
|
|
if (ks == NULL) {
|
|
ks = xcalloc(1, sizeof *ks);
|
|
ks->action = 't';
|
|
ks->delete = 'a';
|
|
ks->format = 32;
|
|
ks->medium = 'd';
|
|
}
|
|
ks->more = 0;
|
|
error = kitty_control(ks, buf, controllen);
|
|
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;
|
|
result->action = ks->action;
|
|
result->delete = ks->delete;
|
|
result->placement_id = ks->placement_id;
|
|
result->z = ks->z;
|
|
result->x = ks->source_x;
|
|
result->y = ks->source_y;
|
|
if (error != 0)
|
|
goto fail;
|
|
if (ks->image_number != 0) {
|
|
if (ks->image_id != 0)
|
|
goto fail;
|
|
result->status = KITTY_PARSE_UNSUPPORTED;
|
|
goto fail;
|
|
}
|
|
if (ks->unsupported) {
|
|
result->status = KITTY_PARSE_UNSUPPORTED;
|
|
goto fail;
|
|
}
|
|
if (ks->medium != 'd') {
|
|
result->status = KITTY_PARSE_UNSUPPORTED;
|
|
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) {
|
|
result->status = KITTY_PARSE_MISSING;
|
|
im = NULL;
|
|
} else if (ks->virtual) {
|
|
im = kitty_place_image(source, ks, xpixel, ypixel);
|
|
image_free(source);
|
|
if (im != NULL) {
|
|
(void)kitty_placement_set(kc, ks, im);
|
|
image_free(im);
|
|
im = NULL;
|
|
result->action = 'u';
|
|
result->status = KITTY_PARSE_OK;
|
|
}
|
|
} else {
|
|
im = kitty_place_image(source, ks, xpixel, ypixel);
|
|
image_free(source);
|
|
if (im != NULL) {
|
|
result->replace_id =
|
|
kitty_placement_set(kc, ks, im);
|
|
result->status = KITTY_PARSE_OK;
|
|
}
|
|
}
|
|
kitty_state_free(ks);
|
|
return (im);
|
|
}
|
|
if (ks->action == 'd') {
|
|
switch (ks->delete) {
|
|
case 'a': case 'A': case 'i': case 'I':
|
|
case 'c': case 'C': case 'p': case 'P':
|
|
case 'q': case 'Q': case 'r': case 'R':
|
|
case 'x': case 'X': case 'y': case 'Y':
|
|
case 'z': case 'Z':
|
|
result->status = KITTY_PARSE_OK;
|
|
break;
|
|
default:
|
|
result->status = KITTY_PARSE_UNSUPPORTED;
|
|
break;
|
|
}
|
|
kitty_state_free(ks);
|
|
return (NULL);
|
|
}
|
|
switch (ks->action) {
|
|
case 'T': case 't': case 'q':
|
|
break;
|
|
case 'f': case 'a': case 'c':
|
|
result->status = KITTY_PARSE_UNSUPPORTED;
|
|
/* FALLTHROUGH */
|
|
default:
|
|
goto fail;
|
|
}
|
|
|
|
/* Decode the complete PNG or raw pixel payload. */
|
|
decoded = ks->raw;
|
|
decodedlen = ks->rawlen;
|
|
ks->raw = NULL;
|
|
if (ks->format == 100) {
|
|
if (ks->compression == 'z') {
|
|
if (ks->data_size == 0 ||
|
|
ks->data_size > IMAGE_SIZE_LIMIT) {
|
|
free(decoded);
|
|
goto fail;
|
|
}
|
|
uncompressedlen = ks->data_size;
|
|
uncompressed = xmalloc(uncompressedlen);
|
|
if (uncompress(uncompressed, &uncompressedlen, decoded,
|
|
decodedlen) != Z_OK ||
|
|
uncompressedlen != ks->data_size) {
|
|
free(decoded);
|
|
free(uncompressed);
|
|
goto fail;
|
|
}
|
|
free(decoded);
|
|
decoded = uncompressed;
|
|
decodedlen = uncompressedlen;
|
|
} else if (ks->compression != '\0') {
|
|
free(decoded);
|
|
goto fail;
|
|
}
|
|
pixels = image_png_decode(decoded, decodedlen, IMAGE_SIZE_LIMIT,
|
|
&ks->width, &ks->height);
|
|
free(decoded);
|
|
} else if (ks->format == 24 || ks->format == 32) {
|
|
pixels = kitty_raw(ks, decoded, decodedlen);
|
|
if (pixels != decoded)
|
|
free(decoded);
|
|
} else {
|
|
free(decoded);
|
|
goto fail;
|
|
}
|
|
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,
|
|
&sx, &sy);
|
|
canvas_width = (uint64_t)sx * cell_width;
|
|
canvas_height = (uint64_t)sy * cell_height;
|
|
if (canvas_width > UINT_MAX || canvas_height > UINT_MAX)
|
|
source = NULL;
|
|
else
|
|
source = image_create(ks->width, ks->height, canvas_width,
|
|
canvas_height, sx, sy, pixels);
|
|
if (source == NULL)
|
|
free(pixels);
|
|
else {
|
|
result->status = KITTY_PARSE_OK;
|
|
|
|
/* 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) {
|
|
im = kitty_place_image(source, ks, xpixel, ypixel);
|
|
if (im == NULL)
|
|
result->status = KITTY_PARSE_ERROR;
|
|
else
|
|
(void)kitty_placement_set(kc, ks, im);
|
|
} else if (ks->virtual) {
|
|
im = kitty_place_image(source, ks, xpixel, ypixel);
|
|
if (im == NULL)
|
|
result->status = KITTY_PARSE_ERROR;
|
|
else {
|
|
(void)kitty_placement_set(kc, ks, im);
|
|
image_free(im);
|
|
im = NULL;
|
|
result->action = 'u';
|
|
}
|
|
} else {
|
|
im = NULL;
|
|
}
|
|
image_free(source);
|
|
}
|
|
kitty_state_free(ks);
|
|
return (im);
|
|
|
|
fail:
|
|
kc->transfer = NULL;
|
|
kitty_state_free(ks);
|
|
return (NULL);
|
|
}
|
|
|
|
/* Decode one UTF-8 character from a Kitty placeholder. */
|
|
static int
|
|
kitty_placeholder_character(const u_char *data, size_t size, size_t *offset,
|
|
uint32_t *value)
|
|
{
|
|
u_char ch;
|
|
u_int needed, i;
|
|
uint32_t result;
|
|
|
|
if (*offset >= size)
|
|
return (0);
|
|
ch = data[(*offset)++];
|
|
if (ch < 0x80) {
|
|
*value = ch;
|
|
return (1);
|
|
}
|
|
if ((ch & 0xe0) == 0xc0) {
|
|
needed = 1;
|
|
result = ch & 0x1f;
|
|
} else if ((ch & 0xf0) == 0xe0) {
|
|
needed = 2;
|
|
result = ch & 0x0f;
|
|
} else if ((ch & 0xf8) == 0xf0) {
|
|
needed = 3;
|
|
result = ch & 0x07;
|
|
} else
|
|
return (0);
|
|
if (needed > size - *offset)
|
|
return (0);
|
|
for (i = 0; i < needed; i++) {
|
|
ch = data[(*offset)++];
|
|
if ((ch & 0xc0) != 0x80)
|
|
return (0);
|
|
result = (result << 6)|(ch & 0x3f);
|
|
}
|
|
*value = result;
|
|
return (1);
|
|
}
|
|
|
|
/* Return the Kitty placeholder diacritic index for a character. */
|
|
static int
|
|
kitty_placeholder_index(uint32_t value, u_int *index)
|
|
{
|
|
u_int i;
|
|
|
|
for (i = 0; i < nitems(kitty_diacritics); i++) {
|
|
if (kitty_diacritics[i] == value) {
|
|
*index = i;
|
|
return (1);
|
|
}
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
/* Return whether a cell starts with a Kitty Unicode placeholder. */
|
|
int
|
|
kitty_cell_is_placeholder(const struct grid_cell *gc)
|
|
{
|
|
if (gc->data.size < 4)
|
|
return (0);
|
|
if (gc->data.data[0] != 0xf4)
|
|
return (0);
|
|
if (gc->data.data[1] != 0x8e)
|
|
return (0);
|
|
if (gc->data.data[2] != 0xbb)
|
|
return (0);
|
|
if (gc->data.data[3] != 0xae)
|
|
return (0);
|
|
return (1);
|
|
}
|
|
|
|
/* Decode an image or placement ID from a placeholder colour. */
|
|
static int
|
|
kitty_colour_id(int colour, u_int *id)
|
|
{
|
|
if (colour < 0)
|
|
return (0);
|
|
if (colour & COLOUR_FLAG_RGB)
|
|
*id = colour & 0xffffff;
|
|
else if (colour & COLOUR_FLAG_256)
|
|
*id = colour & 0xff;
|
|
else if (colour < 8)
|
|
*id = colour;
|
|
else if (colour >= 90) {
|
|
if (colour >= 98)
|
|
return (0);
|
|
*id = colour - 90 + 8;
|
|
} else
|
|
return (0);
|
|
return (1);
|
|
}
|
|
|
|
/* Resolve a Kitty Unicode placeholder to an image and source cell. */
|
|
int
|
|
kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc,
|
|
u_int grid_x, u_int grid_y, struct kitty_placeholder *placeholder)
|
|
{
|
|
struct kitty_context *kc = state;
|
|
struct kitty_source *source;
|
|
struct kitty_placement *placement;
|
|
struct kitty_placeholder left;
|
|
struct grid_cell left_cell;
|
|
struct image *im;
|
|
uint32_t value;
|
|
size_t offset = 0;
|
|
u_int values[3], nvalues = 0, id, x = 0, y = 0;
|
|
u_int sx, sy, placement_id = 0;
|
|
int inherit = 0;
|
|
|
|
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)) {
|
|
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)) {
|
|
return (0);
|
|
}
|
|
if (!kitty_placeholder_index(value, &values[nvalues]))
|
|
return (0);
|
|
nvalues++;
|
|
}
|
|
if (offset != gc->data.size)
|
|
return (0);
|
|
|
|
if (!kitty_colour_id(gc->fg, &id))
|
|
return (0);
|
|
(void)kitty_colour_id(gc->us, &placement_id);
|
|
if (nvalues >= 1)
|
|
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 && left_cell.us == gc->us) {
|
|
inherit = image_grid_get_placeholder(gd,
|
|
grid_x - 1, gd->hsize + grid_y, &left);
|
|
}
|
|
}
|
|
if (inherit) {
|
|
if ((nvalues >= 1 && left.source_y != y) ||
|
|
(nvalues >= 2 && left.source_x + 1 != x)) {
|
|
inherit = 0;
|
|
}
|
|
}
|
|
if (inherit) {
|
|
if (nvalues == 0)
|
|
y = left.source_y;
|
|
if (nvalues < 2)
|
|
x = left.source_x + 1;
|
|
if (nvalues < 3)
|
|
id |= left.image_id & 0xff000000;
|
|
}
|
|
if (nvalues == 3) {
|
|
if (values[2] > 255)
|
|
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);
|
|
for (placement = source->placements; placement != NULL;
|
|
placement = placement->next) {
|
|
if (!placement->virtual)
|
|
continue;
|
|
if (placement_id != 0 &&
|
|
placement->placement_id != placement_id) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (placement != NULL) {
|
|
im = image_find(placement->server_id);
|
|
placeholder->z = placement->z;
|
|
placement_id = placement->placement_id;
|
|
} else
|
|
return (0);
|
|
if (im == NULL)
|
|
return (0);
|
|
image_get_size_in_cells(im, &sx, &sy);
|
|
if (x >= sx || y >= sy)
|
|
return (0);
|
|
|
|
placeholder->image = im;
|
|
placeholder->source_x = x;
|
|
placeholder->source_y = y;
|
|
placeholder->image_id = id;
|
|
placeholder->placement_id = placement_id;
|
|
return (1);
|
|
}
|