tmux/image-sixel.c
Nicholas Marriott 7a78cdf78c Three SIXEL improvements from nincsnevem662 at gmail dot com in GitHub issue
4149:

- Pass P2 on the received image through when forwarding the image to the
  outside terminal to preserve transparency;

- Preserve raster attribute dimensions which may be smaller than the actual
  image (used to crop out parts of the image to work around the limitation that
  rows must come in groups of six);

- To avoid collapsing empty sixel lines, no longer ignore duplicate "-" new
  line specifiers.
2024-10-04 07:49:07 +01:00

629 lines
13 KiB
C

/* $OpenBSD$ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* 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 <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
#define SIXEL_WIDTH_LIMIT 10000
#define SIXEL_HEIGHT_LIMIT 10000
struct sixel_line {
u_int x;
uint16_t *data;
};
struct sixel_image {
u_int x;
u_int y;
u_int xpixel;
u_int ypixel;
u_int set_ra;
u_int ra_x;
u_int ra_y;
u_int *colours;
u_int ncolours;
u_int p2;
u_int dx;
u_int dy;
u_int dc;
struct sixel_line *lines;
};
static int
sixel_parse_expand_lines(struct sixel_image *si, u_int y)
{
if (y <= si->y)
return (0);
if (y > SIXEL_HEIGHT_LIMIT)
return (1);
si->lines = xrecallocarray(si->lines, si->y, y, sizeof *si->lines);
si->y = y;
return (0);
}
static int
sixel_parse_expand_line(struct sixel_image *si, struct sixel_line *sl, u_int x)
{
if (x <= sl->x)
return (0);
if (x > SIXEL_WIDTH_LIMIT)
return (1);
if (x > si->x)
si->x = x;
sl->data = xrecallocarray(sl->data, sl->x, si->x, sizeof *sl->data);
sl->x = si->x;
return (0);
}
static u_int
sixel_get_pixel(struct sixel_image *si, u_int x, u_int y)
{
struct sixel_line *sl;
if (y >= si->y)
return (0);
sl = &si->lines[y];
if (x >= sl->x)
return (0);
return (sl->data[x]);
}
static int
sixel_set_pixel(struct sixel_image *si, u_int x, u_int y, u_int c)
{
struct sixel_line *sl;
if (sixel_parse_expand_lines(si, y + 1) != 0)
return (1);
sl = &si->lines[y];
if (sixel_parse_expand_line(si, sl, x + 1) != 0)
return (1);
sl->data[x] = c;
return (0);
}
static int
sixel_parse_write(struct sixel_image *si, u_int ch)
{
struct sixel_line *sl;
u_int i;
if (sixel_parse_expand_lines(si, si->dy + 6) != 0)
return (1);
sl = &si->lines[si->dy];
for (i = 0; i < 6; i++) {
if (sixel_parse_expand_line(si, sl, si->dx + 1) != 0)
return (1);
if (ch & (1 << i))
sl->data[si->dx] = si->dc;
sl++;
}
return (0);
}
static const char *
sixel_parse_attributes(struct sixel_image *si, const char *cp, const char *end)
{
const char *last;
char *endptr;
u_int x, y;
last = cp;
while (last != end) {
if (*last != ';' && (*last < '0' || *last > '9'))
break;
last++;
}
strtoul(cp, &endptr, 10);
if (endptr == last || *endptr != ';')
return (last);
strtoul(endptr + 1, &endptr, 10);
if (endptr == last)
return (last);
if (*endptr != ';') {
log_debug("%s: missing ;", __func__);
return (NULL);
}
x = strtoul(endptr + 1, &endptr, 10);
if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__);
return (NULL);
}
if (x > SIXEL_WIDTH_LIMIT) {
log_debug("%s: image is too wide", __func__);
return (NULL);
}
y = strtoul(endptr + 1, &endptr, 10);
if (endptr != last) {
log_debug("%s: extra ;", __func__);
return (NULL);
}
if (y > SIXEL_HEIGHT_LIMIT) {
log_debug("%s: image is too tall", __func__);
return (NULL);
}
si->x = x;
sixel_parse_expand_lines(si, y);
si->set_ra = 1;
si->ra_x = x;
si->ra_y = y;
return (last);
}
static const char *
sixel_parse_colour(struct sixel_image *si, const char *cp, const char *end)
{
const char *last;
char *endptr;
u_int c, type, r, g, b;
last = cp;
while (last != end) {
if (*last != ';' && (*last < '0' || *last > '9'))
break;
last++;
}
c = strtoul(cp, &endptr, 10);
if (c > SIXEL_COLOUR_REGISTERS) {
log_debug("%s: too many colours", __func__);
return (NULL);
}
si->dc = c + 1;
if (endptr == last || *endptr != ';')
return (last);
type = strtoul(endptr + 1, &endptr, 10);
if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__);
return (NULL);
}
r = strtoul(endptr + 1, &endptr, 10);
if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__);
return (NULL);
}
g = strtoul(endptr + 1, &endptr, 10);
if (endptr == last || *endptr != ';') {
log_debug("%s: missing ;", __func__);
return (NULL);
}
b = strtoul(endptr + 1, &endptr, 10);
if (endptr != last) {
log_debug("%s: missing ;", __func__);
return (NULL);
}
if (type != 1 && type != 2) {
log_debug("%s: invalid type %d", __func__, type);
return (NULL);
}
if (c + 1 > si->ncolours) {
si->colours = xrecallocarray(si->colours, si->ncolours, c + 1,
sizeof *si->colours);
si->ncolours = c + 1;
}
si->colours[c] = (type << 24) | (r << 16) | (g << 8) | b;
return (last);
}
static const char *
sixel_parse_repeat(struct sixel_image *si, const char *cp, const char *end)
{
const char *last;
char tmp[32], ch;
u_int n = 0, i;
const char *errstr = NULL;
last = cp;
while (last != end) {
if (*last < '0' || *last > '9')
break;
tmp[n++] = *last++;
if (n == (sizeof tmp) - 1) {
log_debug("%s: repeat not terminated", __func__);
return (NULL);
}
}
if (n == 0 || last == end) {
log_debug("%s: repeat not terminated", __func__);
return (NULL);
}
tmp[n] = '\0';
n = strtonum(tmp, 1, SIXEL_WIDTH_LIMIT, &errstr);
if (n == 0 || errstr != NULL) {
log_debug("%s: repeat too wide", __func__);
return (NULL);
}
ch = (*last++) - 0x3f;
for (i = 0; i < n; i++) {
if (sixel_parse_write(si, ch) != 0) {
log_debug("%s: width limit reached", __func__);
return (NULL);
}
si->dx++;
}
return (last);
}
struct sixel_image *
sixel_parse(const char *buf, size_t len, u_int p2, u_int xpixel, u_int ypixel)
{
struct sixel_image *si;
const char *cp = buf, *end = buf + len;
char ch;
if (len == 0 || len == 1 || *cp++ != 'q') {
log_debug("%s: empty image", __func__);
return (NULL);
}
si = xcalloc (1, sizeof *si);
si->xpixel = xpixel;
si->ypixel = ypixel;
si->p2 = p2;
while (cp != end) {
ch = *cp++;
switch (ch) {
case '"':
cp = sixel_parse_attributes(si, cp, end);
if (cp == NULL)
goto bad;
break;
case '#':
cp = sixel_parse_colour(si, cp, end);
if (cp == NULL)
goto bad;
break;
case '!':
cp = sixel_parse_repeat(si, cp, end);
if (cp == NULL)
goto bad;
break;
case '-':
si->dx = 0;
si->dy += 6;
break;
case '$':
si->dx = 0;
break;
default:
if (ch < 0x20)
break;
if (ch < 0x3f || ch > 0x7e)
goto bad;
if (sixel_parse_write(si, ch - 0x3f) != 0) {
log_debug("%s: width limit reached", __func__);
goto bad;
}
si->dx++;
break;
}
}
if (si->x == 0 || si->y == 0)
goto bad;
return (si);
bad:
free(si);
return (NULL);
}
void
sixel_free(struct sixel_image *si)
{
u_int y;
for (y = 0; y < si->y; y++)
free(si->lines[y].data);
free(si->lines);
free(si->colours);
free(si);
}
void
sixel_log(struct sixel_image *si)
{
struct sixel_line *sl;
char s[SIXEL_WIDTH_LIMIT + 1];
u_int i, x, y, cx, cy;
sixel_size_in_cells(si, &cx, &cy);
log_debug("%s: image %ux%u (%ux%u)", __func__, si->x, si->y, cx, cy);
for (i = 0; i < si->ncolours; i++)
log_debug("%s: colour %u is %07x", __func__, i, si->colours[i]);
for (y = 0; y < si->y; y++) {
sl = &si->lines[y];
for (x = 0; x < si->x; x++) {
if (x >= sl->x)
s[x] = '_';
else if (sl->data[x] != 0)
s[x] = '0' + (sl->data[x] - 1) % 10;
else
s[x] = '.';
}
s[x] = '\0';
log_debug("%s: %4u: %s", __func__, y, s);
}
}
void
sixel_size_in_cells(struct sixel_image *si, u_int *x, u_int *y)
{
if ((si->x % si->xpixel) == 0)
*x = (si->x / si->xpixel);
else
*x = 1 + (si->x / si->xpixel);
if ((si->y % si->ypixel) == 0)
*y = (si->y / si->ypixel);
else
*y = 1 + (si->y / si->ypixel);
}
struct sixel_image *
sixel_scale(struct sixel_image *si, u_int xpixel, u_int ypixel, u_int ox,
u_int oy, u_int sx, u_int sy, int colours)
{
struct sixel_image *new;
u_int cx, cy, pox, poy, psx, psy, tsx, tsy, px, py;
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, remembering that we
* can only draw vertical sections of six pixels.
*/
sixel_size_in_cells(si, &cx, &cy);
if (ox >= cx)
return (NULL);
if (oy >= cy)
return (NULL);
if (ox + sx >= cx)
sx = cx - ox;
if (oy + sy >= cy)
sy = cy - oy;
if (xpixel == 0)
xpixel = si->xpixel;
if (ypixel == 0)
ypixel = si->ypixel;
pox = ox * si->xpixel;
poy = oy * si->ypixel;
psx = sx * si->xpixel;
psy = sy * si->ypixel;
tsx = sx * xpixel;
tsy = ((sy * ypixel) / 6) * 6;
new = xcalloc (1, sizeof *si);
new->xpixel = xpixel;
new->ypixel = ypixel;
new->p2 = si->p2;
new->set_ra = si->set_ra;
/* clamp to slice end */
new->ra_x = si->ra_x < psx ? si->ra_x : psx;
new->ra_y = si->ra_y < psy ? si->ra_y : psy;
/* subtract slice origin */
new->ra_x = new->ra_x > pox ? new->ra_x - pox : 0;
new->ra_y = new->ra_y > poy ? new->ra_y - poy : 0;
/* resize */
new->ra_x = new->ra_x * xpixel / si->xpixel;
new->ra_y = new->ra_y * ypixel / si->ypixel;
for (y = 0; y < tsy; y++) {
py = poy + ((double)y * psy / tsy);
for (x = 0; x < tsx; x++) {
px = pox + ((double)x * psx / tsx);
sixel_set_pixel(new, x, y, sixel_get_pixel(si, px, py));
}
}
if (colours) {
new->colours = xmalloc(si->ncolours * sizeof *new->colours);
for (i = 0; i < si->ncolours; i++)
new->colours[i] = si->colours[i];
new->ncolours = si->ncolours;
}
return (new);
}
static void
sixel_print_add(char **buf, size_t *len, size_t *used, const char *s,
size_t slen)
{
if (*used + slen >= *len + 1) {
(*len) *= 2;
*buf = xrealloc(*buf, *len);
}
memcpy(*buf + *used, s, slen);
(*used) += slen;
}
static void
sixel_print_repeat(char **buf, size_t *len, size_t *used, u_int count, char ch)
{
char tmp[16];
size_t tmplen;
if (count == 1)
sixel_print_add(buf, len, used, &ch, 1);
else if (count == 2) {
sixel_print_add(buf, len, used, &ch, 1);
sixel_print_add(buf, len, used, &ch, 1);
} else if (count == 3) {
sixel_print_add(buf, len, used, &ch, 1);
sixel_print_add(buf, len, used, &ch, 1);
sixel_print_add(buf, len, used, &ch, 1);
} else if (count != 0) {
tmplen = xsnprintf(tmp, sizeof tmp, "!%u%c", count, ch);
sixel_print_add(buf, len, used, tmp, tmplen);
}
}
char *
sixel_print(struct sixel_image *si, struct sixel_image *map, size_t *size)
{
char *buf, tmp[64], *contains, data, last = 0;
size_t len, used = 0, tmplen;
u_int *colours, ncolours, i, c, x, y, count;
struct sixel_line *sl;
if (map != NULL) {
colours = map->colours;
ncolours = map->ncolours;
} else {
colours = si->colours;
ncolours = si->ncolours;
}
if (ncolours == 0)
return (NULL);
contains = xcalloc(1, ncolours);
len = 8192;
buf = xmalloc(len);
tmplen = xsnprintf(tmp, sizeof tmp, "\033P0;%uq", si->p2);
sixel_print_add(&buf, &len, &used, tmp, tmplen);
if (si->set_ra) {
tmplen = xsnprintf(tmp, sizeof tmp, "\"1;1;%u;%u", si->ra_x,
si->ra_y);
sixel_print_add(&buf, &len, &used, tmp, tmplen);
}
for (i = 0; i < ncolours; i++) {
c = colours[i];
tmplen = xsnprintf(tmp, sizeof tmp, "#%u;%u;%u;%u;%u",
i, c >> 24, (c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff);
sixel_print_add(&buf, &len, &used, tmp, tmplen);
}
for (y = 0; y < si->y; y += 6) {
memset(contains, 0, ncolours);
for (x = 0; x < si->x; x++) {
for (i = 0; i < 6; i++) {
if (y + i >= si->y)
break;
sl = &si->lines[y + i];
if (x < sl->x && sl->data[x] != 0)
contains[sl->data[x] - 1] = 1;
}
}
for (c = 0; c < ncolours; c++) {
if (!contains[c])
continue;
tmplen = xsnprintf(tmp, sizeof tmp, "#%u", c);
sixel_print_add(&buf, &len, &used, tmp, tmplen);
count = 0;
for (x = 0; x < si->x; x++) {
data = 0;
for (i = 0; i < 6; i++) {
if (y + i >= si->y)
break;
sl = &si->lines[y + i];
if (x < sl->x && sl->data[x] == c + 1)
data |= (1 << i);
}
data += 0x3f;
if (data != last) {
sixel_print_repeat(&buf, &len, &used,
count, last);
last = data;
count = 1;
} else
count++;
}
sixel_print_repeat(&buf, &len, &used, count, data);
sixel_print_add(&buf, &len, &used, "$", 1);
}
if (buf[used - 1] == '$')
used--;
sixel_print_add(&buf, &len, &used, "-", 1);
}
if (buf[used - 1] == '$' || buf[used - 1] == '-')
used--;
sixel_print_add(&buf, &len, &used, "\033\\", 2);
buf[used] = '\0';
if (size != NULL)
*size = used;
free(contains);
return (buf);
}
struct screen *
sixel_to_screen(struct sixel_image *si)
{
struct screen *s;
struct screen_write_ctx ctx;
struct grid_cell gc;
u_int x, y, sx, sy;
sixel_size_in_cells(si, &sx, &sy);
s = xmalloc(sizeof *s);
screen_init(s, sx, sy, 0);
memcpy(&gc, &grid_default_cell, sizeof gc);
gc.attr |= (GRID_ATTR_CHARSET|GRID_ATTR_DIM);
utf8_set(&gc.data, '~');
screen_write_start(&ctx, s);
if (sx == 1 || sy == 1) {
for (y = 0; y < sy; y++) {
for (x = 0; x < sx; x++)
grid_view_set_cell(s->grid, x, y, &gc);
}
} else {
screen_write_box(&ctx, sx, sy, BOX_LINES_DEFAULT, NULL, NULL);
for (y = 1; y < sy - 1; y++) {
for (x = 1; x < sx - 1; x++)
grid_view_set_cell(s->grid, x, y, &gc);
}
}
screen_write_stop(&ctx);
return (s);
}