sixel: add support for overlapping sixels

Writing a sixel on top of an already existing sixel currently has the
following limitations in foot:

* The parts of the first sixel that is covered by the new sixel are
  removed, completely. Even if the new sixel has transparent
  areas. I.e. writing a transparent sixel on top of another
  sixel *replaces* the first sixel with the new sixel, instead of
  layering them on top of each other.

* The second sixel erases the first sixel cell-wise. That is, a sixel
  whose size isn’t a multiple of the cell dimensions will leave
  unsightly holes in the first sixel.

This patch takes care of both issues.

The first one is actually the easiest one: all we need to do is
calculate the intersection, and blend the two images. To keep things
relatively simple, we use the pixman image from the *new* image, and
use the ‘OVER_REVERSE’ operation to blend the new image over the old
one.

That is, the old image is still split into four tiles (top, left,
right, bottom), just like before. But instead of throwing away the
fifth middle tile, we blend it with the new image. As an optimization,
this is only done if the new image has transparency (P1=1).

The second problem is solved by detecting when we’re erasing an area
from the second image that is larger than the new image. In this case,
we enlarge the new image, and copy the old image into the new one.

Finally, when we enlarge the new image, there may be areas in the new
image that is *not* covered by the old image. These areas are made
transparent.

The end result is:

* Each cell is covered by at *most* 1 sixel image. I.e. the total
  numbers of sixels are finite. This is important for the ‘mpv
  --vo=sixel’ use case - we don’t want to end up with thousands of
  sixels layered on top of each other.

* Writing an opaque sixel on top of another sixel has _almost_ zero
  performance impact. Especially if the two sixels have the same size,
  so that we don’t have to resize the new image. Again, important for
  the ‘mpv --vo=sixel’ use case.

Closes #562
This commit is contained in:
Daniel Eklöf 2021-06-06 21:38:31 +02:00
parent ed081f5f3c
commit 6d336fcadd
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
2 changed files with 225 additions and 17 deletions

239
sixel.c
View file

@ -359,9 +359,191 @@ sixel_scroll_down(struct terminal *term, int rows)
verify_sixels(term);
}
static void
blend_new_image_over_old(const struct terminal *term,
const struct sixel *six, pixman_region32_t *six_rect,
int row, int col, pixman_image_t **pix, bool *opaque)
{
xassert(pix != NULL);
xassert(opaque != NULL);
const int six_ofs_x = six->pos.col * term->cell_width;
const int six_ofs_y = six->pos.row * term->cell_height;
const int img_ofs_x = col * term->cell_width;
const int img_ofs_y = row * term->cell_height;
const int img_width = pixman_image_get_width(*pix);
const int img_height = pixman_image_get_height(*pix);
pixman_region32_t pix_rect;
pixman_region32_init_rect(
&pix_rect, img_ofs_x, img_ofs_y, img_width, img_height);
/* Blend the intersection between the old and new images */
pixman_region32_t intersection;
pixman_region32_init(&intersection);
pixman_region32_intersect(&intersection, six_rect, &pix_rect);
int n_rects = -1;
pixman_box32_t *boxes = pixman_region32_rectangles(
&intersection, &n_rects);
if (n_rects == 0)
goto out;
xassert(n_rects == 1);
pixman_box32_t *box = &boxes[0];
if (!*opaque) {
/*
* New image is transparent - blend on top of the old
* sixel image.
*/
pixman_image_composite32(
PIXMAN_OP_OVER_REVERSE,
six->pix, NULL, *pix,
box->x1 - six_ofs_x, box->y1 - six_ofs_y,
0, 0,
box->x1 - img_ofs_x, box->y1 - img_ofs_y,
box->x2 - box->x1, box->y2 - box->y1);
}
/*
* Since the old image is split into sub-tiles on a
* per-row basis, we need to enlarge the new image and
* copy the old image if the old image extends beyond the
* new image.
*
* The "bounding" coordinates are either the edges of the
* old image, or the next cell boundary, whichever comes
* first.
*/
int bounding_x = six_ofs_x + six->width > img_ofs_x + img_width
? min(
six_ofs_x + six->width,
(box->x2 + term->cell_width - 1) / term->cell_width * term->cell_width)
: box->x2;
int bounding_y = six_ofs_y + six->height > img_ofs_y + img_height
? min(
six_ofs_y + six->height,
(box->y2 + term->cell_height - 1) / term->cell_height * term->cell_height)
: box->y2;
/* The required size of the new image */
const int required_width = bounding_x - img_ofs_x;
const int required_height = bounding_y - img_ofs_y;
const int new_width = max(img_width, required_width);
const int new_height = max(img_height, required_height);
if (new_width <= img_width && new_height <= img_height)
goto out;
//LOG_INFO("enlarging: %dx%d -> %dx%d", img_width, img_height, new_width, new_height);
if (!six->opaque) {
/* Transparency is viral */
*opaque = false;
}
/* Create a new pixmap */
int stride = new_width * sizeof(uint32_t);
uint32_t *new_data = xmalloc(stride * new_height);
pixman_image_t *pix2 = pixman_image_create_bits_no_clear(
PIXMAN_a8r8g8b8, new_width, new_height, new_data, stride);
#if defined(_DEBUG)
/* Fill new image with an easy-to-recognize color (green) */
for (size_t i = 0; i < new_width * new_height; i++)
new_data[i] = 0xff00ff00;
#endif
/* Copy the new image, from its old pixmap, to the new pixmap */
pixman_image_composite32(
PIXMAN_OP_SRC,
*pix, NULL, pix2, 0, 0, 0, 0, 0, 0, img_width, img_height);
/* Copy the bottom tile of the old sixel image into the new pixmap */
pixman_image_composite32(
PIXMAN_OP_SRC,
six->pix, NULL, pix2,
box->x1 - six_ofs_x, box->y2 - six_ofs_y,
0, 0,
box->x1 - img_ofs_x, box->y2 - img_ofs_y,
bounding_x - box->x1, bounding_y - box->y2);
/* Copy the right tile of the old sixel image into the new pixmap */
pixman_image_composite32(
PIXMAN_OP_SRC,
six->pix, NULL, pix2,
box->x2 - six_ofs_x, box->y1 - six_ofs_y,
0, 0,
box->x2 - img_ofs_x, box->y1 - img_ofs_y,
bounding_x - box->x2, bounding_y - box->y1);
/*
* Ensure the newly allocated area is initialized.
*
* Some of it, or all, will have been initialized above, by the
* bottom and right tiles from the old sixel image. However, there
* may be areas in the new image that isn't covered by the old
* image. These areas need to be made transparent.
*/
pixman_region32_t uninitialized;
pixman_region32_init_rects(
&uninitialized,
(const pixman_box32_t []){
/* Extended image area on the right side */
{img_ofs_x + img_width, img_ofs_y, img_ofs_x + new_width, img_ofs_y + new_height},
/* Bottom */
{img_ofs_x, img_ofs_y + img_height, img_ofs_x + new_width, img_ofs_y + new_height}},
2);
/* Subtract the old sixel image, since the area(s) covered by the
* old image has already been copied, and *must* not be
* overwritten */
pixman_region32_t diff;
pixman_region32_init(&diff);
pixman_region32_subtract(&diff, &uninitialized, six_rect);
if (pixman_region32_not_empty(&diff)) {
pixman_image_t *src =
pixman_image_create_solid_fill(&(pixman_color_t){0});
int count = -1;
pixman_box32_t *rects = pixman_region32_rectangles(&diff, &count);
for (int i = 0; i < count; i++) {
pixman_image_composite32(
PIXMAN_OP_SRC,
src, NULL, pix2,
0, 0, 0, 0,
rects[i].x1 - img_ofs_x, rects[i].y1 - img_ofs_y,
rects[i].x2 - rects[i].x1,
rects[i].y2 - rects[i].y1);
}
pixman_image_unref(src);
*opaque = false;
}
pixman_region32_fini(&diff);
pixman_region32_fini(&uninitialized);
/* Use the new pixmap in place of the old one */
free(pixman_image_get_data(*pix));
pixman_image_unref(*pix);
*pix = pix2;
out:
pixman_region32_fini(&intersection);
pixman_region32_fini(&pix_rect);
}
static void
sixel_overwrite(struct terminal *term, struct sixel *six,
int row, int col, int height, int width)
int row, int col, int height, int width,
pixman_image_t **pix, bool *opaque)
{
pixman_region32_t six_rect;
pixman_region32_init_rect(
@ -376,13 +558,17 @@ sixel_overwrite(struct terminal *term, struct sixel *six,
width * term->cell_width, height * term->cell_height);
#if defined(_DEBUG)
pixman_region32_t intersection;
pixman_region32_init(&intersection);
pixman_region32_intersect(&intersection, &six_rect, &overwrite_rect);
xassert(pixman_region32_not_empty(&intersection));
pixman_region32_fini(&intersection);
pixman_region32_t cell_intersection;
pixman_region32_init(&cell_intersection);
pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect);
xassert(pixman_region32_not_empty(&cell_intersection));
pixman_region32_fini(&cell_intersection);
#endif
if (pix != NULL)
blend_new_image_over_old(term, six, &six_rect, row, col, pix, opaque);
pixman_region32_t diff;
pixman_region32_init(&diff);
pixman_region32_subtract(&diff, &six_rect, &overwrite_rect);
@ -455,7 +641,8 @@ sixel_overwrite(struct terminal *term, struct sixel *six,
/* Row numbers are absolute */
static void
_sixel_overwrite_by_rectangle(
struct terminal *term, int row, int col, int height, int width)
struct terminal *term, int row, int col, int height, int width,
pixman_image_t **pix, bool *opaque)
{
verify_sixels(term);
@ -519,7 +706,8 @@ _sixel_overwrite_by_rectangle(
struct sixel to_be_erased = *six;
tll_remove(term->grid->sixel_images, it);
sixel_overwrite(term, &to_be_erased, start, col, height, width);
sixel_overwrite(term, &to_be_erased, start, col, height, width,
pix, opaque);
sixel_erase(term, &to_be_erased);
} else
xassert(!collides);
@ -551,10 +739,10 @@ sixel_overwrite_by_rectangle(
if (wraps) {
int rows_to_wrap_around = term->grid->num_rows - start;
xassert(height - rows_to_wrap_around > 0);
_sixel_overwrite_by_rectangle(term, start, col, rows_to_wrap_around, width);
_sixel_overwrite_by_rectangle(term, 0, col, height - rows_to_wrap_around, width);
_sixel_overwrite_by_rectangle(term, start, col, rows_to_wrap_around, width, NULL, NULL);
_sixel_overwrite_by_rectangle(term, 0, col, height - rows_to_wrap_around, width, NULL, NULL);
} else
_sixel_overwrite_by_rectangle(term, start, col, height, width);
_sixel_overwrite_by_rectangle(term, start, col, height, width, NULL, NULL);
term_update_ascii_printer(term);
}
@ -605,7 +793,7 @@ sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width)
struct sixel to_be_erased = *six;
tll_remove(term->grid->sixel_images, it);
sixel_overwrite(term, &to_be_erased, row, col, 1, width);
sixel_overwrite(term, &to_be_erased, row, col, 1, width, NULL, NULL);
sixel_erase(term, &to_be_erased);
}
}
@ -707,7 +895,17 @@ sixel_reflow(struct terminal *term)
/* Sixels that didnt overlap may now do so, which isnt
* allowed of course */
_sixel_overwrite_by_rectangle(
term, six->pos.row, six->pos.col, six->rows, six->cols);
term, six->pos.row, six->pos.col, six->rows, six->cols,
&it->item.pix, &it->item.opaque);
if (it->item.data != pixman_image_get_data(it->item.pix)) {
it->item.data = pixman_image_get_data(it->item.pix);
it->item.width = pixman_image_get_width(it->item.pix);
it->item.height = pixman_image_get_height(it->item.pix);
it->item.cols = (it->item.width + term->cell_width - 1) / term->cell_width;
it->item.rows = (it->item.height + term->cell_height - 1) / term->cell_height;
}
sixel_insert(term, it->item);
}
@ -822,9 +1020,7 @@ sixel_unhook(struct terminal *term)
image.pos.row, image.pos.row + image.rows);
image.pix = pixman_image_create_bits_no_clear(
PIXMAN_a8r8g8b8,
image.width, image.height,
img_data, stride);
PIXMAN_a8r8g8b8, image.width, image.height, img_data, stride);
pixel_row_idx += height;
pixel_rows_left -= height;
@ -865,7 +1061,16 @@ sixel_unhook(struct terminal *term)
}
_sixel_overwrite_by_rectangle(
term, image.pos.row, image.pos.col, image.rows, image.cols);
term, image.pos.row, image.pos.col, image.rows, image.cols,
&image.pix, &image.opaque);
if (image.data != pixman_image_get_data(image.pix)) {
image.data = pixman_image_get_data(image.pix);
image.width = pixman_image_get_width(image.pix);
image.height = pixman_image_get_height(image.pix);
image.cols = (image.width + term->cell_width - 1) / term->cell_width;
image.rows = (image.height + term->cell_height - 1) / term->cell_height;
}
sixel_insert(term, image);