2020-02-21 21:53:23 +01:00
|
|
|
#include "sixel.h"
|
|
|
|
|
|
|
|
|
|
#include <string.h>
|
2020-06-06 13:59:46 +02:00
|
|
|
#include <limits.h>
|
2020-02-21 21:53:23 +01:00
|
|
|
|
|
|
|
|
#define LOG_MODULE "sixel"
|
2024-03-16 08:57:15 +01:00
|
|
|
#define LOG_ENABLE_DBG 0
|
2020-02-21 21:53:23 +01:00
|
|
|
#include "log.h"
|
2021-01-15 20:39:45 +00:00
|
|
|
#include "debug.h"
|
2022-04-25 20:00:14 +02:00
|
|
|
#include "grid.h"
|
2020-11-15 19:45:33 +01:00
|
|
|
#include "hsl.h"
|
2022-04-25 20:00:14 +02:00
|
|
|
#include "render.h"
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
#include "srgb.h"
|
2020-05-01 11:46:24 +02:00
|
|
|
#include "util.h"
|
2020-08-08 20:34:30 +01:00
|
|
|
#include "xmalloc.h"
|
2021-01-14 21:30:06 +00:00
|
|
|
#include "xsnprintf.h"
|
2020-02-21 21:53:23 +01:00
|
|
|
|
|
|
|
|
static size_t count;
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
static void sixel_put_generic(struct terminal *term, uint8_t c);
|
|
|
|
|
static void sixel_put_ar_11(struct terminal *term, uint8_t c);
|
|
|
|
|
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
static uint32_t
|
|
|
|
|
color_decode_srgb(const struct terminal *term, uint16_t r, uint16_t g, uint16_t b)
|
|
|
|
|
{
|
|
|
|
|
if (term->sixel.linear_blending) {
|
|
|
|
|
if (term->sixel.use_10bit) {
|
|
|
|
|
r = srgb_decode_8_to_16(r) >> 6;
|
|
|
|
|
g = srgb_decode_8_to_16(g) >> 6;
|
|
|
|
|
b = srgb_decode_8_to_16(b) >> 6;
|
|
|
|
|
} else {
|
|
|
|
|
r = srgb_decode_8_to_8(r);
|
|
|
|
|
g = srgb_decode_8_to_8(g);
|
|
|
|
|
b = srgb_decode_8_to_8(b);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (term->sixel.use_10bit) {
|
|
|
|
|
r <<= 2;
|
|
|
|
|
g <<= 2;
|
|
|
|
|
b <<= 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t color;
|
|
|
|
|
|
|
|
|
|
if (term->sixel.use_10bit) {
|
|
|
|
|
if (PIXMAN_FORMAT_TYPE(term->sixel.pixman_fmt) == PIXMAN_TYPE_ARGB)
|
|
|
|
|
color = 0x3u << 30 | r << 20 | g << 10 | b;
|
|
|
|
|
else
|
|
|
|
|
color = 0x3u << 30 | b << 20 | g << 10 | r;
|
|
|
|
|
} else
|
|
|
|
|
color = 0xffu << 24 | r << 16 | g << 8 | b;
|
|
|
|
|
|
|
|
|
|
return color;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-10 18:36:54 +02:00
|
|
|
void
|
|
|
|
|
sixel_fini(struct terminal *term)
|
|
|
|
|
{
|
2021-04-07 19:07:43 +02:00
|
|
|
free(term->sixel.image.data);
|
2021-02-16 19:37:49 +01:00
|
|
|
free(term->sixel.private_palette);
|
|
|
|
|
free(term->sixel.shared_palette);
|
2020-06-10 18:36:54 +02:00
|
|
|
}
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
sixel_put
|
2021-03-09 17:23:55 +01:00
|
|
|
sixel_init(struct terminal *term, int p1, int p2, int p3)
|
2020-02-21 21:53:23 +01:00
|
|
|
{
|
2021-03-09 17:23:55 +01:00
|
|
|
/*
|
2024-05-22 14:56:10 +02:00
|
|
|
* P1: pixel aspect ratio
|
|
|
|
|
* - 0,1 - 2:1
|
|
|
|
|
* - 2 - 5:1
|
|
|
|
|
* - 3,4 - 3:1
|
|
|
|
|
* - 5,6 - 2:1
|
|
|
|
|
* - 7,8,9 - 1:1
|
|
|
|
|
*
|
2021-03-09 17:23:55 +01:00
|
|
|
* P2: background color mode
|
|
|
|
|
* - 0|2: empty pixels use current background color
|
|
|
|
|
* - 1: empty pixels remain at their current color (i.e. transparent)
|
|
|
|
|
* P3: horizontal grid size - ignored
|
|
|
|
|
*/
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(term->sixel.image.data == NULL);
|
|
|
|
|
xassert(term->sixel.palette_size <= SIXEL_MAX_COLORS);
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2023-06-19 19:06:38 +02:00
|
|
|
/* Default aspect ratio is 2:1 */
|
|
|
|
|
const int pad = 1;
|
|
|
|
|
const int pan =
|
|
|
|
|
(p1 == 2) ? 5 :
|
|
|
|
|
(p1 == 3 || p1 == 4) ? 3 :
|
|
|
|
|
(p1 == 7 || p1 == 8 || p1 == 9) ? 1 : 2;
|
|
|
|
|
|
|
|
|
|
LOG_DBG("initializing sixel with "
|
2023-06-20 12:58:35 +02:00
|
|
|
"p1=%d (pan=%d, pad=%d, aspect-ratio=%d:%d), "
|
|
|
|
|
"p2=%d (transparent=%s), "
|
2023-06-19 19:06:38 +02:00
|
|
|
"p3=%d (ignored)",
|
2023-06-20 12:58:35 +02:00
|
|
|
p1, pan, pad, pan, pad, p2, p2 == 1 ? "yes" : "no", p3);
|
2023-06-19 19:06:38 +02:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
term->sixel.state = SIXEL_DECSIXEL;
|
2020-02-22 10:54:52 +01:00
|
|
|
term->sixel.pos = (struct coord){0, 0};
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.color_idx = 0;
|
2023-06-19 19:06:38 +02:00
|
|
|
term->sixel.pan = pan;
|
|
|
|
|
term->sixel.pad = pad;
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.param = 0;
|
|
|
|
|
term->sixel.param_idx = 0;
|
|
|
|
|
memset(term->sixel.params, 0, sizeof(term->sixel.params));
|
2021-03-09 17:23:55 +01:00
|
|
|
term->sixel.transparent_bg = p2 == 1;
|
2023-06-19 19:06:38 +02:00
|
|
|
term->sixel.image.data = NULL;
|
2024-03-04 16:40:16 +01:00
|
|
|
term->sixel.image.p = NULL;
|
2023-06-19 19:06:38 +02:00
|
|
|
term->sixel.image.width = 0;
|
2023-06-20 12:58:35 +02:00
|
|
|
term->sixel.image.height = 0;
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
term->sixel.image.alloc_height = 0;
|
2024-03-11 16:25:45 +01:00
|
|
|
term->sixel.image.bottom_pixel = 0;
|
2025-05-01 08:34:49 +02:00
|
|
|
term->sixel.linear_blending = wayl_do_linear_blending(term->wl, term->conf);
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
term->sixel.pixman_fmt = PIXMAN_a8r8g8b8;
|
|
|
|
|
|
2025-05-01 08:34:49 +02:00
|
|
|
if (term->conf->tweak.surface_bit_depth == SHM_BITS_10) {
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) {
|
|
|
|
|
term->sixel.use_10bit = true;
|
|
|
|
|
term->sixel.pixman_fmt = PIXMAN_a2r10g10b10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else if (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010) {
|
|
|
|
|
term->sixel.use_10bit = true;
|
|
|
|
|
term->sixel.pixman_fmt = PIXMAN_a2b10g10r10;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t active_palette_entries = min(
|
|
|
|
|
ALEN(term->conf->colors.sixel), term->sixel.palette_size);
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2021-02-16 19:37:49 +01:00
|
|
|
if (term->sixel.use_private_palette) {
|
2021-02-17 21:58:36 +01:00
|
|
|
xassert(term->sixel.private_palette == NULL);
|
|
|
|
|
term->sixel.private_palette = xcalloc(
|
|
|
|
|
term->sixel.palette_size, sizeof(term->sixel.private_palette[0]));
|
2024-03-15 15:19:43 +01:00
|
|
|
|
|
|
|
|
memcpy(
|
2024-10-23 08:35:30 +02:00
|
|
|
term->sixel.private_palette, term->conf->colors.sixel,
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
active_palette_entries * sizeof(term->sixel.private_palette[0]));
|
|
|
|
|
|
|
|
|
|
if (term->sixel.linear_blending || term->sixel.use_10bit) {
|
|
|
|
|
for (size_t i = 0; i < active_palette_entries; i++) {
|
|
|
|
|
uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff;
|
|
|
|
|
uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff;
|
|
|
|
|
uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff;
|
|
|
|
|
term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-15 15:19:43 +01:00
|
|
|
|
2021-02-16 19:37:49 +01:00
|
|
|
term->sixel.palette = term->sixel.private_palette;
|
|
|
|
|
} else {
|
|
|
|
|
if (term->sixel.shared_palette == NULL) {
|
|
|
|
|
term->sixel.shared_palette = xcalloc(
|
|
|
|
|
term->sixel.palette_size, sizeof(term->sixel.shared_palette[0]));
|
2024-03-15 15:19:43 +01:00
|
|
|
|
|
|
|
|
memcpy(
|
2024-10-23 08:35:30 +02:00
|
|
|
term->sixel.shared_palette, term->conf->colors.sixel,
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
active_palette_entries * sizeof(term->sixel.shared_palette[0]));
|
|
|
|
|
|
|
|
|
|
if (term->sixel.linear_blending || term->sixel.use_10bit) {
|
|
|
|
|
for (size_t i = 0; i < active_palette_entries; i++) {
|
|
|
|
|
uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff;
|
|
|
|
|
uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff;
|
|
|
|
|
uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff;
|
|
|
|
|
term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-02-16 19:37:49 +01:00
|
|
|
} else {
|
|
|
|
|
/* Shared palette - do *not* reset palette for new sixels */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
term->sixel.palette = term->sixel.shared_palette;
|
2020-06-10 18:36:54 +02:00
|
|
|
}
|
|
|
|
|
|
2020-02-21 21:53:23 +01:00
|
|
|
count = 0;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic;
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
|
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
static void
|
|
|
|
|
sixel_invalidate_cache(struct sixel *sixel)
|
|
|
|
|
{
|
|
|
|
|
if (sixel->scaled.pix != NULL)
|
|
|
|
|
pixman_image_unref(sixel->scaled.pix);
|
|
|
|
|
|
|
|
|
|
free(sixel->scaled.data);
|
|
|
|
|
sixel->scaled.pix = NULL;
|
|
|
|
|
sixel->scaled.data = NULL;
|
|
|
|
|
sixel->scaled.width = -1;
|
|
|
|
|
sixel->scaled.height = -1;
|
|
|
|
|
|
|
|
|
|
sixel->pix = NULL;
|
|
|
|
|
sixel->width = -1;
|
|
|
|
|
sixel->height = -1;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-22 00:23:19 +01:00
|
|
|
void
|
|
|
|
|
sixel_destroy(struct sixel *sixel)
|
|
|
|
|
{
|
2023-06-29 14:49:54 +02:00
|
|
|
sixel_invalidate_cache(sixel);
|
2020-02-22 00:23:19 +01:00
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
if (sixel->original.pix != NULL)
|
|
|
|
|
pixman_image_unref(sixel->original.pix);
|
|
|
|
|
|
|
|
|
|
free(sixel->original.data);
|
|
|
|
|
sixel->original.pix = NULL;
|
|
|
|
|
sixel->original.data = NULL;
|
2020-02-22 00:23:19 +01:00
|
|
|
}
|
|
|
|
|
|
2020-06-29 21:53:29 +02:00
|
|
|
void
|
|
|
|
|
sixel_destroy_all(struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
tll_foreach(term->normal.sixel_images, it)
|
|
|
|
|
sixel_destroy(&it->item);
|
|
|
|
|
tll_foreach(term->alt.sixel_images, it)
|
|
|
|
|
sixel_destroy(&it->item);
|
|
|
|
|
tll_free(term->normal.sixel_images);
|
|
|
|
|
tll_free(term->alt.sixel_images);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-24 18:39:14 +01:00
|
|
|
static void
|
|
|
|
|
sixel_erase(struct terminal *term, struct sixel *sixel)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < sixel->rows; i++) {
|
|
|
|
|
int r = (sixel->pos.row + i) & (term->grid->num_rows - 1);
|
|
|
|
|
|
|
|
|
|
struct row *row = term->grid->rows[r];
|
2020-03-06 19:11:57 +01:00
|
|
|
if (row == NULL) {
|
|
|
|
|
/* A resize/reflow may cause row to now be unallocated */
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-24 18:39:14 +01:00
|
|
|
row->dirty = true;
|
|
|
|
|
|
2023-10-12 16:22:50 +02:00
|
|
|
for (int c = sixel->pos.col; c < min(sixel->pos.col + sixel->cols, term->cols); c++)
|
2020-02-24 18:39:14 +01:00
|
|
|
row->cells[c].attrs.clean = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sixel_destroy(sixel);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-05 18:28:53 +02:00
|
|
|
/*
|
|
|
|
|
* Verify the sixels are sorted correctly.
|
|
|
|
|
*
|
|
|
|
|
* The sixels are sorted on their *end* row, in descending order. This
|
|
|
|
|
* invariant means the most recent sixels appear first in the list.
|
|
|
|
|
*/
|
2020-10-04 19:29:48 +02:00
|
|
|
static void
|
2020-10-04 11:00:50 +02:00
|
|
|
verify_list_order(const struct terminal *term)
|
2020-06-29 22:01:02 +02:00
|
|
|
{
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
int prev_row = INT_MAX;
|
2020-07-16 08:10:56 +02:00
|
|
|
int prev_col = -1;
|
|
|
|
|
int prev_col_count = 0;
|
2020-06-28 09:16:43 +02:00
|
|
|
|
2020-10-04 19:10:54 +02:00
|
|
|
/* To aid debugging */
|
2023-02-12 19:09:48 +01:00
|
|
|
size_t UNUSED idx = 0;
|
2020-10-04 19:10:54 +02:00
|
|
|
|
2020-06-28 09:16:43 +02:00
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
2022-04-25 20:00:14 +02:00
|
|
|
int row = grid_row_abs_to_sb(
|
|
|
|
|
term->grid, term->rows, it->item.pos.row + it->item.rows - 1);
|
2020-07-16 08:10:56 +02:00
|
|
|
int col = it->item.pos.col;
|
|
|
|
|
int col_count = it->item.cols;
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(row <= prev_row);
|
2020-07-16 08:10:56 +02:00
|
|
|
|
|
|
|
|
if (row == prev_row) {
|
|
|
|
|
/* Allowed to be on the same row only if their columns
|
|
|
|
|
* don't overlap */
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(col + col_count <= prev_col ||
|
2020-07-16 08:10:56 +02:00
|
|
|
prev_col + prev_col_count <= col);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
prev_row = row;
|
2020-07-16 08:10:56 +02:00
|
|
|
prev_col = col;
|
|
|
|
|
prev_col_count = col_count;
|
2020-10-04 19:10:54 +02:00
|
|
|
idx++;
|
2020-06-29 22:01:02 +02:00
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-05 18:28:53 +02:00
|
|
|
/*
|
|
|
|
|
* Verifies there aren't any sixels that cross the scrollback
|
|
|
|
|
* wrap-around. This invariant means a sixel's absolute row numbers
|
|
|
|
|
* are strictly increasing.
|
|
|
|
|
*/
|
2020-10-04 19:29:48 +02:00
|
|
|
static void
|
2020-10-04 11:00:50 +02:00
|
|
|
verify_no_wraparound_crossover(const struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
|
|
|
|
const struct sixel *six = &it->item;
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(six->pos.row >= 0);
|
|
|
|
|
xassert(six->pos.row < term->grid->num_rows);
|
2020-10-04 11:00:50 +02:00
|
|
|
|
2020-10-04 13:10:06 +02:00
|
|
|
int end = (six->pos.row + six->rows - 1) & (term->grid->num_rows - 1);
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(end >= six->pos.row);
|
2020-10-04 11:00:50 +02:00
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-05 18:29:24 +02:00
|
|
|
/*
|
|
|
|
|
* Verify there aren't any sixels that cross the scrollback end. This
|
|
|
|
|
* invariant means a sixel's rebased row numbers are strictly
|
|
|
|
|
* increasing.
|
|
|
|
|
*/
|
|
|
|
|
static void
|
|
|
|
|
verify_scrollback_consistency(const struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
|
|
|
|
const struct sixel *six = &it->item;
|
|
|
|
|
|
|
|
|
|
int last_row = -1;
|
|
|
|
|
for (int i = 0; i < six->rows; i++) {
|
2022-04-25 20:00:14 +02:00
|
|
|
int row_no = grid_row_abs_to_sb(
|
|
|
|
|
term->grid, term->rows, six->pos.row + i);
|
2020-10-05 18:29:24 +02:00
|
|
|
|
|
|
|
|
if (last_row != -1)
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(last_row < row_no);
|
2020-10-05 18:29:24 +02:00
|
|
|
|
|
|
|
|
last_row = row_no;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-05 18:28:53 +02:00
|
|
|
/*
|
|
|
|
|
* Verifies no sixel overlap with any other sixels.
|
|
|
|
|
*/
|
2020-10-04 19:29:48 +02:00
|
|
|
static void
|
2020-10-04 11:00:50 +02:00
|
|
|
verify_no_overlap(const struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
|
|
|
|
const struct sixel *six1 = &it->item;
|
|
|
|
|
|
|
|
|
|
pixman_region32_t rect1;
|
|
|
|
|
pixman_region32_init_rect(
|
|
|
|
|
&rect1, six1->pos.col, six1->pos.row, six1->cols, six1->rows);
|
|
|
|
|
|
|
|
|
|
tll_foreach(term->grid->sixel_images, it2) {
|
|
|
|
|
const struct sixel *six2 = &it2->item;
|
|
|
|
|
|
|
|
|
|
if (six1 == six2)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
pixman_region32_t rect2;
|
|
|
|
|
pixman_region32_init_rect(
|
|
|
|
|
&rect2, six2->pos.col,
|
|
|
|
|
six2->pos.row, six2->cols, six2->rows);
|
|
|
|
|
|
|
|
|
|
pixman_region32_t intersection;
|
2020-10-04 13:32:29 +02:00
|
|
|
pixman_region32_init(&intersection);
|
2020-10-04 11:00:50 +02:00
|
|
|
pixman_region32_intersect(&intersection, &rect1, &rect2);
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(!pixman_region32_not_empty(&intersection));
|
2020-10-04 13:32:53 +02:00
|
|
|
|
|
|
|
|
pixman_region32_fini(&intersection);
|
|
|
|
|
pixman_region32_fini(&rect2);
|
2020-10-04 11:00:50 +02:00
|
|
|
}
|
2020-10-04 13:32:53 +02:00
|
|
|
|
|
|
|
|
pixman_region32_fini(&rect1);
|
2020-10-04 11:00:50 +02:00
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-04 19:29:48 +02:00
|
|
|
static void
|
2020-10-04 11:00:50 +02:00
|
|
|
verify_sixels(const struct terminal *term)
|
|
|
|
|
{
|
2020-10-04 19:29:48 +02:00
|
|
|
verify_no_wraparound_crossover(term);
|
2020-10-05 18:29:24 +02:00
|
|
|
verify_scrollback_consistency(term);
|
2020-10-04 19:29:48 +02:00
|
|
|
verify_no_overlap(term);
|
|
|
|
|
verify_list_order(term);
|
2020-10-04 11:00:50 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
static void
|
|
|
|
|
sixel_insert(struct terminal *term, struct sixel sixel)
|
|
|
|
|
{
|
2022-04-25 20:00:14 +02:00
|
|
|
int end_row = grid_row_abs_to_sb(
|
|
|
|
|
term->grid, term->rows, sixel.pos.row + sixel.rows - 1);
|
2020-06-28 09:16:43 +02:00
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
2022-04-25 20:00:14 +02:00
|
|
|
int rebased = grid_row_abs_to_sb(
|
|
|
|
|
term->grid, term->rows, it->item.pos.row + it->item.rows - 1);
|
|
|
|
|
|
|
|
|
|
if (rebased < end_row) {
|
2020-06-28 09:16:43 +02:00
|
|
|
tll_insert_before(term->grid->sixel_images, it, sixel);
|
2020-06-28 10:45:05 +02:00
|
|
|
goto out;
|
2020-06-28 09:16:43 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-28 08:37:25 +02:00
|
|
|
tll_push_back(term->grid->sixel_images, sixel);
|
2020-06-28 10:45:05 +02:00
|
|
|
|
|
|
|
|
out:
|
|
|
|
|
#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
|
|
|
|
|
LOG_DBG("sixel list after insertion:");
|
|
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
|
|
|
|
LOG_DBG(" rows=%d+%d", it->item.pos.row, it->item.rows);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2020-10-04 11:00:50 +02:00
|
|
|
verify_sixels(term);
|
2020-06-28 08:37:25 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
void
|
|
|
|
|
sixel_scroll_up(struct terminal *term, int rows)
|
2020-02-22 21:35:45 +01:00
|
|
|
{
|
2020-10-04 19:28:22 +02:00
|
|
|
if (likely(tll_length(term->grid->sixel_images) == 0))
|
|
|
|
|
return;
|
|
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
tll_rforeach(term->grid->sixel_images, it) {
|
2020-02-24 18:39:14 +01:00
|
|
|
struct sixel *six = &it->item;
|
2020-06-06 13:59:46 +02:00
|
|
|
|
2022-04-25 20:00:14 +02:00
|
|
|
int six_start = grid_row_abs_to_sb(term->grid, term->rows, six->pos.row);
|
sixel: scroll-up: don’t break out early of loop
This function loops the list of sixels, and discards those that would
be scrolled out if the grid offset is moved forward by the specified
number of rows.
The criteria is when the rebased row value is less than the number of
rows to scroll.
A rebased row number is a zero-based number starting at the beginning
of the scrollback. Thus, when scrolling 5 rows, rows with a rebased
row number between 0-4 will be scrolled out.
For performance reasons, we used to break out of the loop as soon as a
row number *larger* than the scroll count.
This however does not work. The sixels are sorted by their *end*
row. While this works in most cases (think images outputted in the
shell in the normal screen), it doesn’t always.
In the alt screen, where applications can print images just about
anywhere, it is possible to have *any* start row number anywhere in
the sixel list. Just as long as their *end* row numbers are sorted.
For example, a huuuge sixel that covers the entire scrollback. This
sixel will naturally be first in the list (and thus sixel_scroll_up()
will visit it *last*, since it iterates the list in reverse), but
should still be destroyed when scrolling.
2020-10-04 19:11:40 +02:00
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
if (six_start < rows) {
|
2020-10-04 14:03:45 +02:00
|
|
|
sixel_erase(term, six);
|
2020-06-29 22:01:02 +02:00
|
|
|
tll_remove(term->grid->sixel_images, it);
|
sixel: scroll-up: don’t break out early of loop
This function loops the list of sixels, and discards those that would
be scrolled out if the grid offset is moved forward by the specified
number of rows.
The criteria is when the rebased row value is less than the number of
rows to scroll.
A rebased row number is a zero-based number starting at the beginning
of the scrollback. Thus, when scrolling 5 rows, rows with a rebased
row number between 0-4 will be scrolled out.
For performance reasons, we used to break out of the loop as soon as a
row number *larger* than the scroll count.
This however does not work. The sixels are sorted by their *end*
row. While this works in most cases (think images outputted in the
shell in the normal screen), it doesn’t always.
In the alt screen, where applications can print images just about
anywhere, it is possible to have *any* start row number anywhere in
the sixel list. Just as long as their *end* row numbers are sorted.
For example, a huuuge sixel that covers the entire scrollback. This
sixel will naturally be first in the list (and thus sixel_scroll_up()
will visit it *last*, since it iterates the list in reverse), but
should still be destroyed when scrolling.
2020-10-04 19:11:40 +02:00
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* Unfortunately, we cannot break here.
|
|
|
|
|
*
|
|
|
|
|
* The sixels are sorted on their *end* row. This means
|
|
|
|
|
* there may be a sixel with a top row that will be
|
|
|
|
|
* scrolled out *anywhere* in the list (think of a huuuuge
|
|
|
|
|
* sixel that covers the entire scrollback)
|
|
|
|
|
*/
|
|
|
|
|
//break;
|
|
|
|
|
}
|
2020-02-22 21:35:45 +01:00
|
|
|
}
|
|
|
|
|
|
2024-06-24 01:26:57 +02:00
|
|
|
term->bits_affecting_ascii_printer.sixels =
|
|
|
|
|
tll_length(term->grid->sixel_images) > 0;
|
2021-03-14 19:19:10 +01:00
|
|
|
term_update_ascii_printer(term);
|
2020-10-04 11:00:50 +02:00
|
|
|
verify_sixels(term);
|
2020-06-06 13:59:46 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
void
|
|
|
|
|
sixel_scroll_down(struct terminal *term, int rows)
|
2020-02-24 18:39:14 +01:00
|
|
|
{
|
2020-10-04 19:28:22 +02:00
|
|
|
if (likely(tll_length(term->grid->sixel_images) == 0))
|
|
|
|
|
return;
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(term->grid->num_rows >= rows);
|
2020-02-24 18:39:14 +01:00
|
|
|
|
2020-03-13 18:44:23 +01:00
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
2020-02-24 18:39:14 +01:00
|
|
|
struct sixel *six = &it->item;
|
|
|
|
|
|
2022-04-25 20:00:14 +02:00
|
|
|
int six_end = grid_row_abs_to_sb(
|
|
|
|
|
term->grid, term->rows, six->pos.row + six->rows - 1);
|
2020-06-29 22:01:02 +02:00
|
|
|
if (six_end >= term->grid->num_rows - rows) {
|
2020-10-04 14:03:45 +02:00
|
|
|
sixel_erase(term, six);
|
2020-03-13 18:44:23 +01:00
|
|
|
tll_remove(term->grid->sixel_images, it);
|
2020-06-29 22:01:02 +02:00
|
|
|
} else
|
|
|
|
|
break;
|
2020-06-27 14:43:29 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-24 01:26:57 +02:00
|
|
|
term->bits_affecting_ascii_printer.sixels =
|
|
|
|
|
tll_length(term->grid->sixel_images) > 0;
|
2021-03-14 19:19:10 +01:00
|
|
|
term_update_ascii_printer(term);
|
2020-10-04 11:00:50 +02:00
|
|
|
verify_sixels(term);
|
2020-06-27 13:56:13 +02:00
|
|
|
}
|
|
|
|
|
|
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
2021-06-06 21:38:31 +02:00
|
|
|
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);
|
|
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
/*
|
|
|
|
|
* TODO: handle images being emitted with different cell dimensions
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const int six_ofs_x = six->pos.col * six->cell_width;
|
|
|
|
|
const int six_ofs_y = six->pos.row * six->cell_height;
|
|
|
|
|
const int img_ofs_x = col * six->cell_width;
|
|
|
|
|
const int img_ofs_y = row * six->cell_height;
|
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
2021-06-06 21:38:31 +02:00
|
|
|
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,
|
2023-06-29 14:49:54 +02:00
|
|
|
six->original.pix, NULL, *pix,
|
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
2021-06-06 21:38:31 +02:00
|
|
|
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.
|
|
|
|
|
*/
|
2023-06-29 14:49:54 +02:00
|
|
|
int bounding_x = six_ofs_x + six->original.width > img_ofs_x + img_width
|
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
2021-06-06 21:38:31 +02:00
|
|
|
? min(
|
2023-06-29 14:49:54 +02:00
|
|
|
six_ofs_x + six->original.width,
|
|
|
|
|
(box->x2 + six->cell_width - 1) / six->cell_width * six->cell_width)
|
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
2021-06-06 21:38:31 +02:00
|
|
|
: box->x2;
|
2023-06-29 14:49:54 +02:00
|
|
|
int bounding_y = six_ofs_y + six->original.height > img_ofs_y + img_height
|
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
2021-06-06 21:38:31 +02:00
|
|
|
? min(
|
2023-06-29 14:49:54 +02:00
|
|
|
six_ofs_y + six->original.height,
|
|
|
|
|
(box->y2 + six->cell_height - 1) / six->cell_height * six->cell_height)
|
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
2021-06-06 21:38:31 +02:00
|
|
|
: 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(
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
term->sixel.pixman_fmt, new_width, new_height, new_data, stride);
|
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
2021-06-06 21:38:31 +02:00
|
|
|
|
|
|
|
|
#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,
|
2023-06-29 14:49:54 +02:00
|
|
|
six->original.pix, NULL, pix2,
|
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
2021-06-06 21:38:31 +02:00
|
|
|
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,
|
2023-06-29 14:49:54 +02:00
|
|
|
six->original.pix, NULL, pix2,
|
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
2021-06-06 21:38:31 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-23 21:07:12 +02:00
|
|
|
static void
|
2020-06-27 15:29:47 +02:00
|
|
|
sixel_overwrite(struct terminal *term, struct sixel *six,
|
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
2021-06-06 21:38:31 +02:00
|
|
|
int row, int col, int height, int width,
|
|
|
|
|
pixman_image_t **pix, bool *opaque)
|
2020-06-23 21:07:12 +02:00
|
|
|
{
|
2020-10-05 18:31:25 +02:00
|
|
|
pixman_region32_t six_rect;
|
|
|
|
|
pixman_region32_init_rect(
|
|
|
|
|
&six_rect,
|
2023-06-29 14:49:54 +02:00
|
|
|
six->pos.col * six->cell_width, six->pos.row * six->cell_height,
|
|
|
|
|
six->original.width, six->original.height);
|
2020-10-05 18:31:25 +02:00
|
|
|
|
|
|
|
|
pixman_region32_t overwrite_rect;
|
|
|
|
|
pixman_region32_init_rect(
|
|
|
|
|
&overwrite_rect,
|
2023-06-29 14:49:54 +02:00
|
|
|
col * six->cell_width, row * six->cell_height,
|
|
|
|
|
width * six->cell_width, height * six->cell_height);
|
2020-06-23 21:07:12 +02:00
|
|
|
|
2020-10-05 18:31:25 +02:00
|
|
|
#if defined(_DEBUG)
|
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
2021-06-06 21:38:31 +02:00
|
|
|
pixman_region32_t cell_intersection;
|
|
|
|
|
pixman_region32_init(&cell_intersection);
|
|
|
|
|
pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect);
|
2024-03-07 16:20:29 +01:00
|
|
|
xassert(!pixman_region32_not_empty(&six_rect) ||
|
|
|
|
|
pixman_region32_not_empty(&cell_intersection));
|
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
2021-06-06 21:38:31 +02:00
|
|
|
pixman_region32_fini(&cell_intersection);
|
2020-10-05 18:31:25 +02:00
|
|
|
#endif
|
2020-06-23 21:07:12 +02:00
|
|
|
|
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
2021-06-06 21:38:31 +02:00
|
|
|
if (pix != NULL)
|
|
|
|
|
blend_new_image_over_old(term, six, &six_rect, row, col, pix, opaque);
|
|
|
|
|
|
2020-10-05 18:31:25 +02:00
|
|
|
pixman_region32_t diff;
|
|
|
|
|
pixman_region32_init(&diff);
|
|
|
|
|
pixman_region32_subtract(&diff, &six_rect, &overwrite_rect);
|
2020-06-23 21:07:12 +02:00
|
|
|
|
2020-10-05 18:31:25 +02:00
|
|
|
pixman_region32_fini(&six_rect);
|
|
|
|
|
pixman_region32_fini(&overwrite_rect);
|
2020-06-27 14:26:13 +02:00
|
|
|
|
2020-10-05 18:31:25 +02:00
|
|
|
int n_rects = -1;
|
|
|
|
|
pixman_box32_t *boxes = pixman_region32_rectangles(&diff, &n_rects);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < n_rects; i++) {
|
|
|
|
|
LOG_DBG("box #%d: x1=%d, y1=%d, x2=%d, y2=%d", i,
|
|
|
|
|
boxes[i].x1, boxes[i].y1, boxes[i].x2, boxes[i].y2);
|
2020-06-27 14:26:13 +02:00
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
xassert(boxes[i].x1 % six->cell_width == 0);
|
|
|
|
|
xassert(boxes[i].y1 % six->cell_height == 0);
|
2020-10-03 22:42:34 +02:00
|
|
|
|
2020-10-05 18:31:25 +02:00
|
|
|
/* New image's position, in cells */
|
2023-06-29 14:49:54 +02:00
|
|
|
const int new_col = boxes[i].x1 / six->cell_width;
|
|
|
|
|
const int new_row = boxes[i].y1 / six->cell_height;
|
2020-10-05 18:31:25 +02:00
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(new_row < term->grid->num_rows);
|
2020-10-05 18:31:25 +02:00
|
|
|
|
|
|
|
|
/* New image's width and height, in pixels */
|
|
|
|
|
const int new_width = boxes[i].x2 - boxes[i].x1;
|
|
|
|
|
const int new_height = boxes[i].y2 - boxes[i].y1;
|
|
|
|
|
|
|
|
|
|
uint32_t *new_data = xmalloc(new_width * new_height * sizeof(uint32_t));
|
2023-06-29 14:49:54 +02:00
|
|
|
const uint32_t *old_data = six->original.data;
|
2020-10-05 18:31:25 +02:00
|
|
|
|
|
|
|
|
/* Pixel offsets into old image backing memory */
|
2023-06-29 14:49:54 +02:00
|
|
|
const int x_ofs = boxes[i].x1 - six->pos.col * six->cell_width;
|
|
|
|
|
const int y_ofs = boxes[i].y1 - six->pos.row * six->cell_height;
|
2020-10-05 18:31:25 +02:00
|
|
|
|
|
|
|
|
/* Copy image data, one row at a time */
|
|
|
|
|
for (size_t j = 0; j < new_height; j++) {
|
|
|
|
|
memcpy(
|
|
|
|
|
&new_data[(0 + j) * new_width],
|
2023-06-29 14:49:54 +02:00
|
|
|
&old_data[(y_ofs + j) * six->original.width + x_ofs],
|
2020-10-05 18:31:25 +02:00
|
|
|
new_width * sizeof(uint32_t));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pixman_image_t *new_pix = pixman_image_create_bits_no_clear(
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t));
|
2020-10-05 18:31:25 +02:00
|
|
|
|
|
|
|
|
struct sixel new_six = {
|
2023-06-29 14:49:54 +02:00
|
|
|
.pix = NULL,
|
|
|
|
|
.width = -1,
|
|
|
|
|
.height = -1,
|
2020-10-05 18:31:25 +02:00
|
|
|
.pos = {.col = new_col, .row = new_row},
|
2023-06-29 14:49:54 +02:00
|
|
|
.cols = (new_width + six->cell_width - 1) / six->cell_width,
|
|
|
|
|
.rows = (new_height + six->cell_height - 1) / six->cell_height,
|
2021-03-09 17:23:55 +01:00
|
|
|
.opaque = six->opaque,
|
2023-06-29 14:49:54 +02:00
|
|
|
.cell_width = six->cell_width,
|
|
|
|
|
.cell_height = six->cell_height,
|
|
|
|
|
.original = {
|
|
|
|
|
.data = new_data,
|
|
|
|
|
.pix = new_pix,
|
|
|
|
|
.width = new_width,
|
|
|
|
|
.height = new_height,
|
|
|
|
|
},
|
|
|
|
|
.scaled = {
|
|
|
|
|
.data = NULL,
|
|
|
|
|
.pix = NULL,
|
|
|
|
|
.width = -1,
|
|
|
|
|
.height = -1,
|
|
|
|
|
},
|
2020-10-05 18:31:25 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
/* Assert we don't cross the scrollback wrap-around */
|
|
|
|
|
const int new_end = new_six.pos.row + new_six.rows - 1;
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(new_end < term->grid->num_rows);
|
2020-10-05 18:31:25 +02:00
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
sixel_insert(term, new_six);
|
2020-06-23 21:07:12 +02:00
|
|
|
}
|
2020-10-05 18:31:25 +02:00
|
|
|
|
|
|
|
|
pixman_region32_fini(&diff);
|
2020-06-23 21:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-27 13:56:13 +02:00
|
|
|
/* Row numbers are absolute */
|
2020-06-27 12:45:22 +02:00
|
|
|
static void
|
2020-06-27 15:29:47 +02:00
|
|
|
_sixel_overwrite_by_rectangle(
|
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
2021-06-06 21:38:31 +02:00
|
|
|
struct terminal *term, int row, int col, int height, int width,
|
|
|
|
|
pixman_image_t **pix, bool *opaque)
|
2020-06-23 21:07:12 +02:00
|
|
|
{
|
2020-10-05 18:30:23 +02:00
|
|
|
verify_sixels(term);
|
|
|
|
|
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
pixman_region32_t overwrite_rect;
|
|
|
|
|
pixman_region32_init_rect(&overwrite_rect, col, row, width, height);
|
|
|
|
|
#endif
|
|
|
|
|
|
2020-06-27 13:56:13 +02:00
|
|
|
const int start = row;
|
|
|
|
|
const int end = row + height - 1;
|
2020-06-23 21:07:12 +02:00
|
|
|
|
2020-10-04 14:05:37 +02:00
|
|
|
/* We should never generate scrollback wrapping sixels */
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(end < term->grid->num_rows);
|
2020-10-04 14:05:37 +02:00
|
|
|
|
2022-04-25 20:00:14 +02:00
|
|
|
const int scrollback_rel_start = grid_row_abs_to_sb(
|
|
|
|
|
term->grid, term->rows, start);
|
2020-06-28 19:22:23 +02:00
|
|
|
|
2020-10-05 18:30:23 +02:00
|
|
|
bool UNUSED would_have_breaked = false;
|
|
|
|
|
|
2020-06-23 21:07:12 +02:00
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
|
|
|
|
struct sixel *six = &it->item;
|
|
|
|
|
|
|
|
|
|
const int six_start = six->pos.row;
|
2020-10-04 14:05:37 +02:00
|
|
|
const int six_end = (six_start + six->rows - 1);
|
2022-04-25 20:00:14 +02:00
|
|
|
const int six_scrollback_rel_end =
|
|
|
|
|
grid_row_abs_to_sb(term->grid, term->rows, six_end);
|
2020-06-27 14:19:08 +02:00
|
|
|
|
2020-10-04 14:05:37 +02:00
|
|
|
/* We should never generate scrollback wrapping sixels */
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(six_end < term->grid->num_rows);
|
2020-10-04 14:05:37 +02:00
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
if (six_scrollback_rel_end < scrollback_rel_start) {
|
2020-06-28 19:22:23 +02:00
|
|
|
/* All remaining sixels are *before* our rectangle */
|
2020-10-05 18:30:23 +02:00
|
|
|
would_have_breaked = true;
|
2020-06-28 19:22:23 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-05 18:30:23 +02:00
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
pixman_region32_t six_rect;
|
|
|
|
|
pixman_region32_init_rect(&six_rect, six->pos.col, six->pos.row, six->cols, six->rows);
|
|
|
|
|
|
|
|
|
|
pixman_region32_t intersection;
|
|
|
|
|
pixman_region32_init(&intersection);
|
|
|
|
|
pixman_region32_intersect(&intersection, &six_rect, &overwrite_rect);
|
|
|
|
|
|
|
|
|
|
const bool collides = pixman_region32_not_empty(&intersection);
|
|
|
|
|
#else
|
|
|
|
|
const bool UNUSED collides = false;
|
|
|
|
|
#endif
|
|
|
|
|
|
2020-06-27 14:19:08 +02:00
|
|
|
if ((start <= six_start && end >= six_start) || /* Crosses sixel start boundary */
|
|
|
|
|
(start <= six_end && end >= six_end) || /* Crosses sixel end boundary */
|
|
|
|
|
(start >= six_start && end <= six_end)) /* Fully within sixel range */
|
2020-06-27 12:45:22 +02:00
|
|
|
{
|
2020-06-23 21:07:12 +02:00
|
|
|
const int col_start = six->pos.col;
|
2020-06-27 12:45:22 +02:00
|
|
|
const int col_end = six->pos.col + six->cols - 1;
|
2020-06-23 21:07:12 +02:00
|
|
|
|
2020-06-27 12:45:22 +02:00
|
|
|
if ((col <= col_start && col + width - 1 >= col_start) ||
|
|
|
|
|
(col <= col_end && col + width - 1 >= col_end) ||
|
|
|
|
|
(col >= col_start && col + width - 1 <= col_end))
|
|
|
|
|
{
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(!would_have_breaked);
|
2020-10-05 18:30:23 +02:00
|
|
|
|
2020-07-23 18:15:29 +02:00
|
|
|
struct sixel to_be_erased = *six;
|
2020-06-23 21:07:12 +02:00
|
|
|
tll_remove(term->grid->sixel_images, it);
|
2020-07-23 18:15:29 +02:00
|
|
|
|
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
2021-06-06 21:38:31 +02:00
|
|
|
sixel_overwrite(term, &to_be_erased, start, col, height, width,
|
|
|
|
|
pix, opaque);
|
2020-07-23 18:15:29 +02:00
|
|
|
sixel_erase(term, &to_be_erased);
|
2020-10-05 18:30:23 +02:00
|
|
|
} else
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(!collides);
|
2020-10-05 18:30:23 +02:00
|
|
|
} else
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(!collides);
|
2020-10-05 18:30:23 +02:00
|
|
|
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
pixman_region32_fini(&intersection);
|
|
|
|
|
pixman_region32_fini(&six_rect);
|
|
|
|
|
#endif
|
2020-06-23 21:07:12 +02:00
|
|
|
}
|
2020-10-05 18:30:23 +02:00
|
|
|
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
pixman_region32_fini(&overwrite_rect);
|
|
|
|
|
#endif
|
2020-06-23 21:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-27 15:22:31 +02:00
|
|
|
void
|
2020-06-27 15:29:47 +02:00
|
|
|
sixel_overwrite_by_rectangle(
|
2020-06-27 15:22:31 +02:00
|
|
|
struct terminal *term, int row, int col, int height, int width)
|
2020-06-27 13:56:13 +02:00
|
|
|
{
|
2020-06-27 14:43:29 +02:00
|
|
|
if (likely(tll_length(term->grid->sixel_images) == 0))
|
|
|
|
|
return;
|
|
|
|
|
|
2020-06-27 15:22:31 +02:00
|
|
|
const int start = (term->grid->offset + row) & (term->grid->num_rows - 1);
|
2020-06-27 13:56:13 +02:00
|
|
|
const int end = (start + height - 1) & (term->grid->num_rows - 1);
|
|
|
|
|
const bool wraps = end < start;
|
|
|
|
|
|
|
|
|
|
if (wraps) {
|
|
|
|
|
int rows_to_wrap_around = term->grid->num_rows - start;
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(height - rows_to_wrap_around > 0);
|
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
2021-06-06 21:38:31 +02:00
|
|
|
_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);
|
2020-06-27 13:56:13 +02:00
|
|
|
} else
|
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
2021-06-06 21:38:31 +02:00
|
|
|
_sixel_overwrite_by_rectangle(term, start, col, height, width, NULL, NULL);
|
2021-03-14 19:19:10 +01:00
|
|
|
|
2024-06-24 01:26:57 +02:00
|
|
|
term->bits_affecting_ascii_printer.sixels =
|
|
|
|
|
tll_length(term->grid->sixel_images) > 0;
|
2021-03-14 19:19:10 +01:00
|
|
|
term_update_ascii_printer(term);
|
2020-06-27 13:56:13 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-28 14:24:30 +02:00
|
|
|
/* Row numbers are relative to grid offset */
|
|
|
|
|
void
|
|
|
|
|
sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width)
|
2020-06-27 13:56:13 +02:00
|
|
|
{
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(col >= 0);
|
2020-06-27 13:56:13 +02:00
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(_row >= 0);
|
|
|
|
|
xassert(_row < term->rows);
|
|
|
|
|
xassert(col >= 0);
|
|
|
|
|
xassert(col < term->grid->num_cols);
|
2020-06-27 14:19:08 +02:00
|
|
|
|
2020-06-27 13:56:13 +02:00
|
|
|
if (likely(tll_length(term->grid->sixel_images) == 0))
|
|
|
|
|
return;
|
|
|
|
|
|
2020-07-07 10:42:59 +02:00
|
|
|
if (col + width > term->grid->num_cols)
|
|
|
|
|
width = term->grid->num_cols - col;
|
|
|
|
|
|
2020-06-28 14:24:30 +02:00
|
|
|
const int row = (term->grid->offset + _row) & (term->grid->num_rows - 1);
|
2022-04-25 20:00:14 +02:00
|
|
|
const int scrollback_rel_row = grid_row_abs_to_sb(term->grid, term->rows, row);
|
2020-06-28 14:24:30 +02:00
|
|
|
|
2020-06-27 13:56:13 +02:00
|
|
|
tll_foreach(term->grid->sixel_images, it) {
|
|
|
|
|
struct sixel *six = &it->item;
|
|
|
|
|
const int six_start = six->pos.row;
|
|
|
|
|
const int six_end = (six_start + six->rows - 1) & (term->grid->num_rows - 1);
|
|
|
|
|
|
2020-06-27 14:19:08 +02:00
|
|
|
/* We should never generate scrollback wrapping sixels */
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(six_end >= six_start);
|
2020-06-27 13:56:13 +02:00
|
|
|
|
2022-04-25 20:00:14 +02:00
|
|
|
const int six_scrollback_rel_end =
|
|
|
|
|
grid_row_abs_to_sb(term->grid, term->rows, six_end);
|
2020-06-28 14:24:30 +02:00
|
|
|
|
2020-06-29 22:01:02 +02:00
|
|
|
if (six_scrollback_rel_end < scrollback_rel_row) {
|
2020-06-28 14:24:30 +02:00
|
|
|
/* All remaining sixels are *before* "our" row */
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-27 14:19:08 +02:00
|
|
|
if (row >= six_start && row <= six_end) {
|
2020-06-27 13:56:13 +02:00
|
|
|
const int col_start = six->pos.col;
|
2020-06-27 15:22:31 +02:00
|
|
|
const int col_end = six->pos.col + six->cols - 1;
|
2020-06-27 13:56:13 +02:00
|
|
|
|
2020-06-27 15:22:31 +02:00
|
|
|
if ((col <= col_start && col + width - 1 >= col_start) ||
|
|
|
|
|
(col <= col_end && col + width - 1 >= col_end) ||
|
|
|
|
|
(col >= col_start && col + width - 1 <= col_end))
|
|
|
|
|
{
|
2020-07-23 18:15:29 +02:00
|
|
|
struct sixel to_be_erased = *six;
|
2020-06-27 13:56:13 +02:00
|
|
|
tll_remove(term->grid->sixel_images, it);
|
2020-07-23 18:15:29 +02:00
|
|
|
|
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
2021-06-06 21:38:31 +02:00
|
|
|
sixel_overwrite(term, &to_be_erased, row, col, 1, width, NULL, NULL);
|
2020-07-23 18:15:29 +02:00
|
|
|
sixel_erase(term, &to_be_erased);
|
2020-06-27 13:56:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-14 19:19:10 +01:00
|
|
|
|
2024-06-24 01:26:57 +02:00
|
|
|
term->bits_affecting_ascii_printer.sixels =
|
|
|
|
|
tll_length(term->grid->sixel_images) > 0;
|
2021-03-14 19:19:10 +01:00
|
|
|
term_update_ascii_printer(term);
|
2020-06-27 13:56:13 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-27 15:22:31 +02:00
|
|
|
void
|
2020-06-28 11:01:19 +02:00
|
|
|
sixel_overwrite_at_cursor(struct terminal *term, int width)
|
2020-06-27 15:22:31 +02:00
|
|
|
{
|
2021-03-14 14:19:12 +01:00
|
|
|
if (likely(tll_length(term->grid->sixel_images) == 0))
|
|
|
|
|
return;
|
|
|
|
|
|
2020-06-27 15:29:47 +02:00
|
|
|
sixel_overwrite_by_row(
|
2020-06-28 11:01:19 +02:00
|
|
|
term, term->grid->cursor.point.row, term->grid->cursor.point.col, width);
|
2020-06-23 21:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
2020-10-04 13:09:24 +02:00
|
|
|
void
|
|
|
|
|
sixel_cell_size_changed(struct terminal *term)
|
|
|
|
|
{
|
2023-06-29 14:49:54 +02:00
|
|
|
tll_foreach(term->normal.sixel_images, it)
|
|
|
|
|
sixel_invalidate_cache(&it->item);
|
2020-10-04 13:09:24 +02:00
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
tll_foreach(term->alt.sixel_images, it)
|
|
|
|
|
sixel_invalidate_cache(&it->item);
|
|
|
|
|
}
|
2020-10-04 13:09:24 +02:00
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
void
|
|
|
|
|
sixel_sync_cache(const struct terminal *term, struct sixel *six)
|
|
|
|
|
{
|
|
|
|
|
if (six->pix != NULL) {
|
|
|
|
|
#if defined(_DEBUG)
|
|
|
|
|
if (six->cell_width == term->cell_width &&
|
|
|
|
|
six->cell_height == term->cell_height)
|
|
|
|
|
{
|
|
|
|
|
xassert(six->pix == six->original.pix);
|
|
|
|
|
xassert(six->width == six->original.width);
|
|
|
|
|
xassert(six->height == six->original.height);
|
|
|
|
|
|
|
|
|
|
xassert(six->scaled.data == NULL);
|
|
|
|
|
xassert(six->scaled.pix == NULL);
|
|
|
|
|
xassert(six->scaled.width < 0);
|
|
|
|
|
xassert(six->scaled.height < 0);
|
|
|
|
|
} else {
|
|
|
|
|
xassert(six->pix == six->scaled.pix);
|
|
|
|
|
xassert(six->width == six->scaled.width);
|
|
|
|
|
xassert(six->height == six->scaled.height);
|
|
|
|
|
|
|
|
|
|
xassert(six->scaled.data != NULL);
|
|
|
|
|
xassert(six->scaled.pix != NULL);
|
|
|
|
|
|
|
|
|
|
/* TODO: check ratio */
|
|
|
|
|
xassert(six->scaled.width >= 0);
|
|
|
|
|
xassert(six->scaled.height >= 0);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
return;
|
2020-10-04 13:09:24 +02:00
|
|
|
}
|
|
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
/* Cache should be invalid */
|
|
|
|
|
xassert(six->scaled.data == NULL);
|
|
|
|
|
xassert(six->scaled.pix == NULL);
|
|
|
|
|
xassert(six->scaled.width < 0);
|
|
|
|
|
xassert(six->scaled.height < 0);
|
|
|
|
|
|
|
|
|
|
if (six->cell_width == term->cell_width &&
|
|
|
|
|
six->cell_height == term->cell_height)
|
|
|
|
|
{
|
|
|
|
|
six->pix = six->original.pix;
|
|
|
|
|
six->width = six->original.width;
|
|
|
|
|
six->height = six->original.height;
|
|
|
|
|
} else {
|
|
|
|
|
const double width_ratio = (double)term->cell_width / six->cell_width;
|
|
|
|
|
const double height_ratio = (double)term->cell_height / six->cell_height;
|
|
|
|
|
|
|
|
|
|
struct pixman_f_transform scale;
|
|
|
|
|
pixman_f_transform_init_scale(
|
|
|
|
|
&scale, 1. / width_ratio, 1. / height_ratio);
|
|
|
|
|
|
|
|
|
|
struct pixman_transform _scale;
|
|
|
|
|
pixman_transform_from_pixman_f_transform(&_scale, &scale);
|
|
|
|
|
pixman_image_set_transform(six->original.pix, &_scale);
|
|
|
|
|
pixman_image_set_filter(six->original.pix, PIXMAN_FILTER_BILINEAR, NULL, 0);
|
|
|
|
|
|
|
|
|
|
int scaled_width = (double)six->original.width * width_ratio;
|
|
|
|
|
int scaled_height = (double)six->original.height * height_ratio;
|
|
|
|
|
int scaled_stride = scaled_width * sizeof(uint32_t);
|
|
|
|
|
|
|
|
|
|
LOG_DBG("scaling sixel: %dx%d -> %dx%d",
|
|
|
|
|
six->original.width, six->original.height,
|
|
|
|
|
scaled_width, scaled_height);
|
|
|
|
|
|
|
|
|
|
uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride);
|
|
|
|
|
pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear(
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
term->sixel.pixman_fmt, scaled_width, scaled_height,
|
2023-06-29 14:49:54 +02:00
|
|
|
(uint32_t *)scaled_data, scaled_stride);
|
|
|
|
|
|
|
|
|
|
pixman_image_composite32(
|
|
|
|
|
PIXMAN_OP_SRC, six->original.pix, NULL, scaled_pix, 0, 0, 0, 0,
|
|
|
|
|
0, 0, scaled_width, scaled_height);
|
|
|
|
|
|
|
|
|
|
pixman_image_set_transform(six->original.pix, NULL);
|
|
|
|
|
|
|
|
|
|
six->scaled.data = scaled_data;
|
|
|
|
|
six->scaled.pix = six->pix = scaled_pix;
|
|
|
|
|
six->scaled.width = six->width = scaled_width;
|
|
|
|
|
six->scaled.height = six->height = scaled_height;
|
|
|
|
|
}
|
2020-10-04 13:09:24 +02:00
|
|
|
}
|
|
|
|
|
|
2020-10-04 13:12:44 +02:00
|
|
|
void
|
2022-10-09 16:01:11 +02:00
|
|
|
sixel_reflow_grid(struct terminal *term, struct grid *grid)
|
2020-10-04 13:12:44 +02:00
|
|
|
{
|
2022-10-09 16:01:11 +02:00
|
|
|
/* Meh - the sixel functions we call use term->grid... */
|
|
|
|
|
struct grid *active_grid = term->grid;
|
|
|
|
|
term->grid = grid;
|
2020-10-04 13:12:44 +02:00
|
|
|
|
2024-02-06 12:36:45 +01:00
|
|
|
/* Need the "real" list to be empty from the beginning */
|
2022-10-09 16:01:11 +02:00
|
|
|
tll(struct sixel) copy = tll_init();
|
|
|
|
|
tll_foreach(grid->sixel_images, it)
|
|
|
|
|
tll_push_back(copy, it->item);
|
|
|
|
|
tll_free(grid->sixel_images);
|
2020-10-04 13:12:44 +02:00
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
tll_rforeach(copy, it) {
|
|
|
|
|
struct sixel *six = &it->item;
|
|
|
|
|
int start = six->pos.row;
|
|
|
|
|
int end = (start + six->rows - 1) & (grid->num_rows - 1);
|
2020-10-04 13:12:44 +02:00
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
if (end < start) {
|
|
|
|
|
/* Crosses scrollback wrap-around */
|
|
|
|
|
/* TODO: split image */
|
|
|
|
|
sixel_destroy(six);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-10-04 13:12:44 +02:00
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
if (six->rows > grid->num_rows) {
|
|
|
|
|
/* Image too large */
|
|
|
|
|
/* TODO: keep bottom part? */
|
|
|
|
|
sixel_destroy(six);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-10-04 13:12:44 +02:00
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
/* Drop sixels that now cross the current scrollback end
|
|
|
|
|
* border. This is similar to a sixel that have been
|
|
|
|
|
* scrolled out */
|
|
|
|
|
/* TODO: should be possible to optimize this */
|
|
|
|
|
bool sixel_destroyed = false;
|
|
|
|
|
int last_row = -1;
|
2020-10-04 13:12:44 +02:00
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
for (int j = 0; j < six->rows; j++) {
|
|
|
|
|
int row_no = grid_row_abs_to_sb(
|
|
|
|
|
term->grid, term->rows, six->pos.row + j);
|
|
|
|
|
if (last_row != -1 && last_row >= row_no) {
|
2020-10-04 13:12:44 +02:00
|
|
|
sixel_destroy(six);
|
2022-10-09 16:01:11 +02:00
|
|
|
sixel_destroyed = true;
|
|
|
|
|
break;
|
2020-10-05 18:33:50 +02:00
|
|
|
}
|
|
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
last_row = row_no;
|
|
|
|
|
}
|
2020-10-05 18:33:50 +02:00
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
if (sixel_destroyed) {
|
|
|
|
|
LOG_WARN("destroyed sixel that now crossed history");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
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
2021-06-06 21:38:31 +02:00
|
|
|
|
2024-02-06 12:36:45 +01:00
|
|
|
/* Sixels that didn't overlap may now do so, which isn't
|
2022-10-09 16:01:11 +02:00
|
|
|
* allowed of course */
|
|
|
|
|
_sixel_overwrite_by_rectangle(
|
|
|
|
|
term, six->pos.row, six->pos.col, six->rows, six->cols,
|
2023-06-29 14:49:54 +02:00
|
|
|
&it->item.original.pix, &it->item.opaque);
|
|
|
|
|
|
|
|
|
|
if (it->item.original.data != pixman_image_get_data(it->item.original.pix)) {
|
|
|
|
|
it->item.original.data = pixman_image_get_data(it->item.original.pix);
|
|
|
|
|
it->item.original.width = pixman_image_get_width(it->item.original.pix);
|
|
|
|
|
it->item.original.height = pixman_image_get_height(it->item.original.pix);
|
|
|
|
|
it->item.cols = (it->item.original.width + it->item.cell_width - 1) / it->item.cell_width;
|
|
|
|
|
it->item.rows = (it->item.original.height + it->item.cell_height - 1) / it->item.cell_height;
|
|
|
|
|
sixel_invalidate_cache(&it->item);
|
2020-10-04 13:12:44 +02:00
|
|
|
}
|
|
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
sixel_insert(term, it->item);
|
2020-10-04 13:12:44 +02:00
|
|
|
}
|
|
|
|
|
|
2022-10-09 16:01:11 +02:00
|
|
|
tll_free(copy);
|
|
|
|
|
term->grid = active_grid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_reflow(struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
for (size_t i = 0; i < 2; i++) {
|
|
|
|
|
struct grid *grid = i == 0 ? &term->normal : &term->alt;
|
|
|
|
|
sixel_reflow_grid(term, grid);
|
|
|
|
|
}
|
2020-10-04 13:12:44 +02:00
|
|
|
}
|
|
|
|
|
|
2020-02-21 21:53:23 +01:00
|
|
|
void
|
|
|
|
|
sixel_unhook(struct terminal *term)
|
|
|
|
|
{
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
if (term->sixel.pos.row < term->sixel.image.height &&
|
|
|
|
|
term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.height)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
* Handle case where image has had its size set by raster
|
|
|
|
|
* attributes, and then one or more sixels were printed on the
|
|
|
|
|
* last row of the RA area.
|
|
|
|
|
*
|
|
|
|
|
* In this case, the image height may not be a multiple of
|
|
|
|
|
* 6*pan. But the printed sixels may still be outside the RA
|
|
|
|
|
* area. In this case, using the size from the RA would
|
|
|
|
|
* truncate the image.
|
|
|
|
|
*
|
|
|
|
|
* So, extend the image to a multiple of 6*pan.
|
|
|
|
|
*
|
|
|
|
|
* If this is a transparent image, the image may get trimmed
|
|
|
|
|
* below (most likely back the size set by RA).
|
|
|
|
|
*/
|
|
|
|
|
term->sixel.image.height = term->sixel.image.alloc_height;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-11 16:25:45 +01:00
|
|
|
/* Strip trailing fully transparent rows, *unless* we *ended* with
|
|
|
|
|
* a trailing GNL, in which case we do *not* want to strip all 6
|
|
|
|
|
* pixel rows */
|
|
|
|
|
if (term->sixel.pos.col > 0) {
|
|
|
|
|
const int bits = sizeof(term->sixel.image.bottom_pixel) * 8;
|
|
|
|
|
const int leading_zeroes = term->sixel.image.bottom_pixel == 0
|
|
|
|
|
? bits
|
|
|
|
|
: __builtin_clz(term->sixel.image.bottom_pixel);
|
|
|
|
|
const int rows_to_trim = leading_zeroes + 6 - bits;
|
|
|
|
|
|
|
|
|
|
LOG_DBG("bottom-pixel: 0x%02x, bits=%d, leading-zeroes=%d, "
|
|
|
|
|
"rows-to-trim=%d*%d", term->sixel.image.bottom_pixel,
|
|
|
|
|
bits, leading_zeroes, rows_to_trim, term->sixel.pan);
|
|
|
|
|
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
/*
|
|
|
|
|
* If the current graphical cursor position is at the last row
|
|
|
|
|
* of the image, *and* the image is transparent (P2=1), trim
|
|
|
|
|
* the entire image.
|
|
|
|
|
*
|
|
|
|
|
* If the image is not transparent, then we can't trim the RA
|
|
|
|
|
* region (it is supposed to "erase", with the current
|
|
|
|
|
* background color.)
|
|
|
|
|
*
|
|
|
|
|
* We *do* "trim" transparent rows from the graphical cursor
|
|
|
|
|
* position, as this affects the positioning of the text
|
|
|
|
|
* cursor.
|
|
|
|
|
*
|
|
|
|
|
* See https://raw.githubusercontent.com/hackerb9/vt340test/main/sixeltests/p2effect.sh
|
|
|
|
|
*/
|
2024-03-15 17:23:21 +01:00
|
|
|
if (term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.alloc_height) {
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
LOG_DBG("trimming image");
|
2024-03-15 17:23:21 +01:00
|
|
|
const int trimmed_height =
|
|
|
|
|
term->sixel.image.alloc_height - rows_to_trim * term->sixel.pan;
|
|
|
|
|
|
|
|
|
|
if (term->sixel.transparent_bg) {
|
|
|
|
|
/* Image is transparent - trim as much as possible */
|
|
|
|
|
term->sixel.image.height = trimmed_height;
|
|
|
|
|
} else {
|
|
|
|
|
/* Image is opaque. We can't trim anything "inside"
|
|
|
|
|
the RA region */
|
|
|
|
|
if (trimmed_height > term->sixel.image.height) {
|
|
|
|
|
/* There are non-empty pixels *outside* the RA
|
|
|
|
|
region - trim up to that point */
|
|
|
|
|
term->sixel.image.height = trimmed_height;
|
|
|
|
|
}
|
|
|
|
|
}
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
} else {
|
|
|
|
|
LOG_DBG("only adjusting cursor position");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
term->sixel.pos.row += 6 * term->sixel.pan;
|
|
|
|
|
term->sixel.pos.row -= rows_to_trim * term->sixel.pan;
|
2024-03-11 16:25:45 +01:00
|
|
|
}
|
|
|
|
|
|
2020-06-27 14:19:08 +02:00
|
|
|
int pixel_row_idx = 0;
|
|
|
|
|
int pixel_rows_left = term->sixel.image.height;
|
|
|
|
|
const int stride = term->sixel.image.width * sizeof(uint32_t);
|
|
|
|
|
|
2020-10-02 20:56:44 +02:00
|
|
|
/*
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
* When sixel scrolling is enabled (the default), sixels behave
|
|
|
|
|
* pretty much like normal output; the sixel starts at the current
|
|
|
|
|
* cursor position and the cursor is moved to a point after the
|
|
|
|
|
* sixel.
|
2020-10-02 20:56:44 +02:00
|
|
|
*
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
* Furthermore, if the sixel reaches the bottom of the scrolling
|
|
|
|
|
* region, the terminal content is scrolled.
|
|
|
|
|
*
|
|
|
|
|
* When scrolling is disabled, sixels always start at (0,0), the
|
|
|
|
|
* cursor is not moved at all, and the terminal content never
|
|
|
|
|
* scrolls.
|
2020-10-02 20:56:44 +02:00
|
|
|
*/
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
|
|
|
|
|
const bool do_scroll = term->sixel.scrolling;
|
|
|
|
|
|
|
|
|
|
/* Number of rows we're allowed to use.
|
|
|
|
|
*
|
|
|
|
|
* When scrolling is enabled, we always allow the entire sixel to
|
|
|
|
|
* be emitted.
|
|
|
|
|
*
|
|
|
|
|
* When disabled, only the number of screen rows may be used. */
|
|
|
|
|
int rows_avail = do_scroll
|
|
|
|
|
? (term->sixel.image.height + term->cell_height - 1) / term->cell_height
|
2021-02-26 14:20:00 +01:00
|
|
|
: term->scroll_region.end;
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
|
|
|
|
|
/* Initial sixel coordinates */
|
|
|
|
|
int start_row = do_scroll ? term->grid->cursor.point.row : 0;
|
|
|
|
|
const int start_col = do_scroll ? term->grid->cursor.point.col : 0;
|
2020-10-02 20:56:44 +02:00
|
|
|
|
2024-03-07 16:19:56 +01:00
|
|
|
/* Total number of rows needed by image */
|
2021-05-08 19:03:08 +02:00
|
|
|
const int rows_needed =
|
2024-03-07 16:19:56 +01:00
|
|
|
(term->sixel.image.height + term->cell_height - 1) / term->cell_height;
|
2021-05-08 19:03:08 +02:00
|
|
|
|
2021-06-24 17:08:33 +02:00
|
|
|
bool free_image_data = true;
|
2021-04-07 19:08:29 +02:00
|
|
|
|
2020-06-27 14:19:08 +02:00
|
|
|
/* We do not allow sixels to cross the scrollback wrap-around, as
|
|
|
|
|
* this makes intersection calculations much more complicated */
|
2021-05-08 19:03:08 +02:00
|
|
|
while (pixel_rows_left > 0 &&
|
|
|
|
|
rows_avail > 0 &&
|
|
|
|
|
rows_needed <= term->grid->num_rows)
|
|
|
|
|
{
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
const int cur_row = (term->grid->offset + start_row) & (term->grid->num_rows - 1);
|
|
|
|
|
const int rows_left_until_wrap_around = term->grid->num_rows - cur_row;
|
|
|
|
|
const int usable_rows = min(rows_avail, rows_left_until_wrap_around);
|
2020-06-27 14:19:08 +02:00
|
|
|
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
const int pixel_rows_avail = usable_rows * term->cell_height;
|
2020-06-27 14:19:08 +02:00
|
|
|
|
|
|
|
|
const int width = term->sixel.image.width;
|
|
|
|
|
const int height = min(pixel_rows_left, pixel_rows_avail);
|
|
|
|
|
|
|
|
|
|
uint32_t *img_data;
|
2021-06-24 17:08:33 +02:00
|
|
|
if (pixel_row_idx == 0 && height == pixel_rows_left) {
|
|
|
|
|
/* Entire image will be emitted as a single chunk - reuse
|
|
|
|
|
* the source buffer */
|
2020-06-27 14:19:08 +02:00
|
|
|
img_data = term->sixel.image.data;
|
2021-06-24 17:08:33 +02:00
|
|
|
free_image_data = false;
|
|
|
|
|
} else {
|
|
|
|
|
xassert(free_image_data);
|
2020-08-08 20:34:30 +01:00
|
|
|
img_data = xmalloc(height * stride);
|
2020-06-27 14:19:08 +02:00
|
|
|
memcpy(
|
|
|
|
|
img_data,
|
|
|
|
|
&((uint8_t *)term->sixel.image.data)[pixel_row_idx * stride],
|
|
|
|
|
height * stride);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct sixel image = {
|
2023-06-29 14:49:54 +02:00
|
|
|
.pix = NULL,
|
|
|
|
|
.width = -1,
|
|
|
|
|
.height = -1,
|
2020-06-27 14:19:08 +02:00
|
|
|
.rows = (height + term->cell_height - 1) / term->cell_height,
|
|
|
|
|
.cols = (width + term->cell_width - 1) / term->cell_width,
|
2020-10-02 20:56:44 +02:00
|
|
|
.pos = (struct coord){start_col, cur_row},
|
2021-03-09 17:23:55 +01:00
|
|
|
.opaque = !term->sixel.transparent_bg,
|
2023-06-29 14:49:54 +02:00
|
|
|
.cell_width = term->cell_width,
|
|
|
|
|
.cell_height = term->cell_height,
|
|
|
|
|
.original = {
|
|
|
|
|
.data = img_data,
|
|
|
|
|
.pix = NULL,
|
|
|
|
|
.width = width,
|
|
|
|
|
.height = height,
|
|
|
|
|
},
|
|
|
|
|
.scaled = {
|
|
|
|
|
.data = NULL,
|
|
|
|
|
.pix = NULL,
|
|
|
|
|
.width = -1,
|
|
|
|
|
.height = -1,
|
|
|
|
|
},
|
2020-06-27 14:19:08 +02:00
|
|
|
};
|
|
|
|
|
|
2021-05-08 19:02:33 +02:00
|
|
|
xassert(image.rows <= term->grid->num_rows);
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(image.pos.row + image.rows - 1 < term->grid->num_rows);
|
2020-06-27 14:19:08 +02:00
|
|
|
|
2021-05-16 10:57:43 +02:00
|
|
|
LOG_DBG("generating %s %dx%d pixman image at %d-%d",
|
|
|
|
|
image.opaque ? "opaque" : "transparent",
|
2024-03-07 16:18:35 +01:00
|
|
|
image.original.width, image.original.height,
|
2020-10-02 20:54:45 +02:00
|
|
|
image.pos.row, image.pos.row + image.rows);
|
2020-06-27 14:19:08 +02:00
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
image.original.pix = pixman_image_create_bits_no_clear(
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
term->sixel.pixman_fmt, image.original.width, image.original.height,
|
2023-06-29 14:49:54 +02:00
|
|
|
img_data, stride);
|
2020-06-27 14:19:08 +02:00
|
|
|
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
pixel_row_idx += height;
|
|
|
|
|
pixel_rows_left -= height;
|
|
|
|
|
rows_avail -= image.rows;
|
|
|
|
|
|
2023-06-16 16:20:37 +02:00
|
|
|
if (do_scroll) {
|
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:
a) scroll terminal content to ensure the new image fits
b) position the text cursor
Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.
Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.
In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.
This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.
The fix is this:
* Always scroll by the number of rows occupied by the image, minus
one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-22 22:01:51 +02:00
|
|
|
/*
|
|
|
|
|
* Linefeeds - always one less than the number of rows
|
|
|
|
|
* occupied by the image.
|
|
|
|
|
*
|
|
|
|
|
* Unless this is *not* the last chunk. In that case,
|
|
|
|
|
* linefeed past the chunk, so that the next chunk
|
|
|
|
|
* "starts" at a "new" row.
|
|
|
|
|
*/
|
2023-06-20 14:52:17 +02:00
|
|
|
const int linefeed_count = rows_avail == 0
|
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:
a) scroll terminal content to ensure the new image fits
b) position the text cursor
Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.
Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.
In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.
This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.
The fix is this:
* Always scroll by the number of rows occupied by the image, minus
one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-22 22:01:51 +02:00
|
|
|
? max(0, image.rows - 1)
|
|
|
|
|
: image.rows;
|
2023-06-20 14:52:17 +02:00
|
|
|
|
2023-06-29 14:49:54 +02:00
|
|
|
xassert(rows_avail == 0 ||
|
|
|
|
|
image.original.height % term->cell_height == 0);
|
2023-06-20 14:52:17 +02:00
|
|
|
|
2023-06-16 16:20:37 +02:00
|
|
|
for (size_t i = 0; i < linefeed_count; i++)
|
|
|
|
|
term_linefeed(term);
|
|
|
|
|
|
|
|
|
|
/* Position text cursor if this is the last image chunk */
|
|
|
|
|
if (rows_avail == 0) {
|
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:
a) scroll terminal content to ensure the new image fits
b) position the text cursor
Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.
Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.
In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.
This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.
The fix is this:
* Always scroll by the number of rows occupied by the image, minus
one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-22 22:01:51 +02:00
|
|
|
int row = term->grid->cursor.point.row;
|
|
|
|
|
|
|
|
|
|
/*
|
2024-03-04 16:18:49 +01:00
|
|
|
* Position the text cursor based on the text row
|
|
|
|
|
* touched by the last sixel
|
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:
a) scroll terminal content to ensure the new image fits
b) position the text cursor
Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.
Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.
In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.
This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.
The fix is this:
* Always scroll by the number of rows occupied by the image, minus
one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-22 22:01:51 +02:00
|
|
|
*/
|
2024-03-15 17:39:43 +01:00
|
|
|
const int pixel_rows = pixel_rows_left > 0
|
|
|
|
|
? image.original.height
|
|
|
|
|
: term->sixel.pos.row;
|
2024-03-04 16:18:49 +01:00
|
|
|
const int term_rows =
|
2024-03-15 17:39:43 +01:00
|
|
|
(pixel_rows + term->cell_height - 1) / term->cell_height;
|
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:
a) scroll terminal content to ensure the new image fits
b) position the text cursor
Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.
Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.
In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.
This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.
The fix is this:
* Always scroll by the number of rows occupied by the image, minus
one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-22 22:01:51 +02:00
|
|
|
|
2023-06-22 22:12:02 +02:00
|
|
|
xassert(term_rows <= image.rows);
|
|
|
|
|
|
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:
a) scroll terminal content to ensure the new image fits
b) position the text cursor
Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.
Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.
In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.
This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.
The fix is this:
* Always scroll by the number of rows occupied by the image, minus
one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-22 22:01:51 +02:00
|
|
|
row -= (image.rows - term_rows);
|
|
|
|
|
|
2023-06-16 16:20:37 +02:00
|
|
|
term_cursor_to(
|
|
|
|
|
term,
|
sixel: ensure enough rows have been scrolled in, to fit the image
When emitting a sixel, we need to:
a) scroll terminal content to ensure the new image fits
b) position the text cursor
Recent changes in the cursor positioning logic meant we reduced the
number of linefeeds, to ensure 1) sixels could be printed to the
bottom row without scrolling the terminal contents, and 2) the cursor
was positioned on the last sixel row.
Except, we’re not actually positioning the cursor on the last sixel
row. We’re positioning it on the text row that maps to the *upper*
pixel of the last sixel.
In most cases, this _is_ the last row of the sixel. But for certain
combinations of font and image sizes, it may be higher up.
This patch fixes a regression, where the terminal contents weren’t
scrolled up enough for certain images, causing a crash when trying to
dirty a not-yet scrolled in row.
The fix is this:
* Always scroll by the number of rows occupied by the image, minus
one. This ensures the image "fits".
* Adjust the cursor position, if necessary.
2023-06-22 22:01:51 +02:00
|
|
|
max(0, row),
|
2023-06-16 16:20:37 +02:00
|
|
|
(term->sixel.cursor_right_of_graphics
|
|
|
|
|
? min(image.pos.col + image.cols, term->cols - 1)
|
|
|
|
|
: image.pos.col));
|
|
|
|
|
}
|
2024-03-15 17:39:43 +01:00
|
|
|
|
|
|
|
|
term->sixel.pos.row -= image.original.height;
|
2023-06-16 16:20:37 +02:00
|
|
|
}
|
|
|
|
|
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
/* Dirty touched cells, and scroll terminal content if necessary */
|
2020-11-13 16:54:40 +01:00
|
|
|
for (size_t i = 0; i < image.rows; i++) {
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
struct row *row = term->grid->rows[cur_row + i];
|
2020-11-13 16:54:40 +01:00
|
|
|
row->dirty = true;
|
|
|
|
|
|
|
|
|
|
for (int col = image.pos.col;
|
|
|
|
|
col < min(image.pos.col + image.cols, term->cols);
|
|
|
|
|
col++)
|
|
|
|
|
{
|
|
|
|
|
row->cells[col].attrs.clean = 0;
|
|
|
|
|
}
|
2021-02-16 19:11:38 +01:00
|
|
|
|
2020-11-13 16:54:40 +01:00
|
|
|
}
|
2021-02-16 19:11:38 +01:00
|
|
|
|
2020-10-05 18:32:44 +02:00
|
|
|
_sixel_overwrite_by_rectangle(
|
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
2021-06-06 21:38:31 +02:00
|
|
|
term, image.pos.row, image.pos.col, image.rows, image.cols,
|
2023-06-29 14:49:54 +02:00
|
|
|
&image.original.pix, &image.opaque);
|
|
|
|
|
|
|
|
|
|
if (image.original.data != pixman_image_get_data(image.original.pix)) {
|
|
|
|
|
image.original.data = pixman_image_get_data(image.original.pix);
|
|
|
|
|
image.original.width = pixman_image_get_width(image.original.pix);
|
|
|
|
|
image.original.height = pixman_image_get_height(image.original.pix);
|
|
|
|
|
image.cols = (image.original.width + image.cell_width - 1) / image.cell_width;
|
|
|
|
|
image.rows = (image.original.height + image.cell_height - 1) / image.cell_height;
|
|
|
|
|
sixel_invalidate_cache(&image);
|
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
2021-06-06 21:38:31 +02:00
|
|
|
}
|
2020-10-05 18:32:44 +02:00
|
|
|
|
2020-06-28 08:37:25 +02:00
|
|
|
sixel_insert(term, image);
|
2020-06-27 14:19:08 +02:00
|
|
|
|
sixel: implement private mode 80 - sixel scrolling
When enabled (the default), sixels behave much like normal output; the
start where the cursor is, and the cursor moves with the
sixel. I.e. after emitting a sixel the cursor is left after the image;
either to the right, if private mode 8452 is enabled, or otherwise on
the next line. Terminal content is scrolled up if the sixel is larger
than the screen.
When disabled, sixels *always* start at (0,0), the cursor never moves,
and the terminal content never scrolls.
In other words, the ‘disabled’ mode is a much simpler mode.
All we need to do to support both modes is re-write the sixel-emitting
loop to:
* break early if we’re “out of rows”, i.e. we’ve reached the bottom of
the screen.
* not linefeed, or move the cursor when scrolling is disabled
This patch also fixes a bug in the (new) implementation of private
mode 8452.
When emitting a sixel, we may break it up into smaller pieces, to
ensure a single sixel (as tracked internally) does not cross the
scrollback wrap-around.
The code that checked if we should do a linefeed or not, would skip
the linefeed on the last row of *each* such sixel piece. The correct
thing to do is to skip it only on the last row of the *last* piece.
I chose not to fix this bug in a separate patch since doing so would
have meant re-writing it again when implementing private mode 80.
2021-02-26 09:28:03 +01:00
|
|
|
if (do_scroll)
|
|
|
|
|
start_row = term->grid->cursor.point.row;
|
|
|
|
|
else
|
|
|
|
|
start_row -= image.rows;
|
2020-06-27 14:19:08 +02:00
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2021-06-24 17:08:33 +02:00
|
|
|
if (free_image_data)
|
|
|
|
|
free(term->sixel.image.data);
|
|
|
|
|
|
2020-02-22 10:46:35 +01:00
|
|
|
term->sixel.image.data = NULL;
|
2024-03-04 16:40:16 +01:00
|
|
|
term->sixel.image.p = NULL;
|
2020-02-22 10:46:35 +01:00
|
|
|
term->sixel.image.width = 0;
|
|
|
|
|
term->sixel.image.height = 0;
|
2020-02-22 10:54:52 +01:00
|
|
|
term->sixel.pos = (struct coord){0, 0};
|
2020-10-04 13:13:40 +02:00
|
|
|
|
2021-02-17 21:58:36 +01:00
|
|
|
free(term->sixel.private_palette);
|
|
|
|
|
term->sixel.private_palette = NULL;
|
|
|
|
|
|
2020-10-04 19:17:33 +02:00
|
|
|
LOG_DBG("you now have %zu sixels in current grid",
|
|
|
|
|
tll_length(term->grid->sixel_images));
|
|
|
|
|
|
2024-06-24 01:26:57 +02:00
|
|
|
|
|
|
|
|
term->bits_affecting_ascii_printer.sixels =
|
|
|
|
|
tll_length(term->grid->sixel_images) > 0;
|
2021-03-14 19:19:10 +01:00
|
|
|
term_update_ascii_printer(term);
|
2020-10-04 13:13:40 +02:00
|
|
|
render_refresh(term);
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
static void ALWAYS_INLINE inline
|
|
|
|
|
memset_u32(uint32_t *data, uint32_t value, size_t count)
|
|
|
|
|
{
|
|
|
|
|
static_assert(sizeof(wchar_t) == 4, "wchar_t is not 4 bytes");
|
|
|
|
|
wmemset((wchar_t *)data, (wchar_t)value, count);
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-03 19:20:41 +01:00
|
|
|
static void
|
2024-03-25 16:33:15 +01:00
|
|
|
resize_horizontally(struct terminal *term, int new_width_mutable)
|
2021-03-07 14:44:28 +01:00
|
|
|
{
|
2024-03-25 16:33:15 +01:00
|
|
|
if (unlikely(new_width_mutable > term->sixel.max_width)) {
|
2022-02-03 19:20:41 +01:00
|
|
|
LOG_WARN("maximum image dimensions exceeded, truncating");
|
2024-03-25 16:33:15 +01:00
|
|
|
new_width_mutable = term->sixel.max_width;
|
2021-03-07 14:44:28 +01:00
|
|
|
}
|
|
|
|
|
|
2024-04-15 16:05:56 +02:00
|
|
|
if (unlikely(term->sixel.image.width >= new_width_mutable))
|
2022-02-03 19:20:41 +01:00
|
|
|
return;
|
|
|
|
|
|
2023-06-20 12:58:35 +02:00
|
|
|
const int sixel_row_height = 6 * term->sixel.pan;
|
|
|
|
|
|
2021-03-07 14:44:28 +01:00
|
|
|
uint32_t *old_data = term->sixel.image.data;
|
|
|
|
|
const int old_width = term->sixel.image.width;
|
2024-03-25 16:33:15 +01:00
|
|
|
const int new_width = new_width_mutable;
|
2021-03-07 14:44:28 +01:00
|
|
|
|
2023-06-20 12:58:35 +02:00
|
|
|
int height;
|
|
|
|
|
if (unlikely(term->sixel.image.height == 0)) {
|
|
|
|
|
/* Lazy initialize height on first printed sixel */
|
|
|
|
|
xassert(old_width == 0);
|
|
|
|
|
term->sixel.image.height = height = sixel_row_height;
|
2024-04-15 16:05:56 +02:00
|
|
|
term->sixel.image.alloc_height = sixel_row_height;
|
2023-06-20 12:58:35 +02:00
|
|
|
} else
|
|
|
|
|
height = term->sixel.image.height;
|
|
|
|
|
|
|
|
|
|
LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)",
|
|
|
|
|
term->sixel.image.width, term->sixel.image.height,
|
|
|
|
|
new_width, height);
|
|
|
|
|
|
2023-06-19 19:06:38 +02:00
|
|
|
int alloc_height = (height + sixel_row_height - 1) / sixel_row_height * sixel_row_height;
|
2021-03-07 14:44:28 +01:00
|
|
|
|
2024-04-15 16:05:56 +02:00
|
|
|
xassert(new_width >= old_width);
|
2021-03-07 16:53:27 +01:00
|
|
|
xassert(new_width > 0);
|
|
|
|
|
xassert(alloc_height > 0);
|
|
|
|
|
|
2021-03-07 14:44:28 +01:00
|
|
|
/* Width (and thus stride) change - need to allocate a new buffer */
|
|
|
|
|
uint32_t *new_data = xmalloc(new_width * alloc_height * sizeof(uint32_t));
|
|
|
|
|
|
2024-10-23 08:35:30 +02:00
|
|
|
uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0];
|
2021-03-09 17:23:55 +01:00
|
|
|
|
2021-03-07 14:44:28 +01:00
|
|
|
/* Copy old rows, and initialize new columns to background color */
|
2024-03-25 16:33:15 +01:00
|
|
|
const uint32_t *end = &new_data[alloc_height * new_width];
|
|
|
|
|
for (uint32_t *n = new_data, *o = old_data;
|
|
|
|
|
n < end;
|
|
|
|
|
n += new_width, o += old_width)
|
|
|
|
|
{
|
|
|
|
|
memcpy(n, o, old_width * sizeof(uint32_t));
|
|
|
|
|
memset_u32(&n[old_width], bg, new_width - old_width);
|
2021-03-07 14:44:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
free(old_data);
|
|
|
|
|
|
|
|
|
|
term->sixel.image.data = new_data;
|
|
|
|
|
term->sixel.image.width = new_width;
|
2024-03-04 16:40:16 +01:00
|
|
|
|
|
|
|
|
const int ofs = term->sixel.pos.row * new_width + term->sixel.pos.col;
|
|
|
|
|
term->sixel.image.p = &term->sixel.image.data[ofs];
|
2021-03-07 14:44:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool
|
2024-03-25 16:33:15 +01:00
|
|
|
resize_vertically(struct terminal *term, const int new_height)
|
2021-03-07 14:44:28 +01:00
|
|
|
{
|
|
|
|
|
LOG_DBG("resizing image vertically: (%d)x%d -> (%d)x%d",
|
|
|
|
|
term->sixel.image.width, term->sixel.image.height,
|
|
|
|
|
term->sixel.image.width, new_height);
|
|
|
|
|
|
|
|
|
|
if (unlikely(new_height > term->sixel.max_height)) {
|
|
|
|
|
LOG_WARN("maximum image dimensions reached");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t *old_data = term->sixel.image.data;
|
|
|
|
|
const int width = term->sixel.image.width;
|
|
|
|
|
const int old_height = term->sixel.image.height;
|
2024-03-04 16:29:04 +01:00
|
|
|
const int sixel_row_height = 6 * term->sixel.pan;
|
2021-03-07 14:44:28 +01:00
|
|
|
|
2024-03-04 16:29:04 +01:00
|
|
|
int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height;
|
2021-03-07 14:44:28 +01:00
|
|
|
|
2021-03-07 16:53:27 +01:00
|
|
|
xassert(new_height > 0);
|
|
|
|
|
|
2023-06-19 19:06:38 +02:00
|
|
|
if (unlikely(width == 0)) {
|
|
|
|
|
xassert(term->sixel.image.data == NULL);
|
|
|
|
|
term->sixel.image.height = new_height;
|
2024-04-15 16:05:56 +02:00
|
|
|
term->sixel.image.alloc_height = alloc_height;
|
2023-06-19 19:06:38 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-07 14:44:28 +01:00
|
|
|
uint32_t *new_data = realloc(
|
|
|
|
|
old_data, width * alloc_height * sizeof(uint32_t));
|
|
|
|
|
|
|
|
|
|
if (new_data == NULL) {
|
|
|
|
|
LOG_ERRNO("failed to reallocate sixel image buffer");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-23 08:35:30 +02:00
|
|
|
const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0];
|
2021-03-09 17:23:55 +01:00
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
memset_u32(&new_data[old_height * width],
|
|
|
|
|
bg,
|
|
|
|
|
(alloc_height - old_height) * width);
|
2021-03-07 14:44:28 +01:00
|
|
|
|
|
|
|
|
term->sixel.image.height = new_height;
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
term->sixel.image.alloc_height = alloc_height;
|
2024-03-04 16:40:16 +01:00
|
|
|
|
|
|
|
|
const int ofs =
|
|
|
|
|
term->sixel.pos.row * term->sixel.image.width + term->sixel.pos.col;
|
|
|
|
|
|
|
|
|
|
term->sixel.image.data = new_data;
|
|
|
|
|
term->sixel.image.p = &term->sixel.image.data[ofs];
|
|
|
|
|
|
2021-03-07 14:44:28 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-22 10:46:35 +01:00
|
|
|
static bool
|
2024-03-25 16:33:15 +01:00
|
|
|
resize(struct terminal *term, int new_width_mutable, int new_height_mutable)
|
2020-02-22 10:46:35 +01:00
|
|
|
{
|
|
|
|
|
LOG_DBG("resizing image: %dx%d -> %dx%d",
|
|
|
|
|
term->sixel.image.width, term->sixel.image.height,
|
2024-04-15 16:05:56 +02:00
|
|
|
new_width_mutable, new_height_mutable);
|
2020-02-22 10:46:35 +01:00
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
if (unlikely(new_width_mutable > term->sixel.max_width)) {
|
2022-02-03 19:20:41 +01:00
|
|
|
LOG_WARN("maximum image width exceeded, truncating");
|
2024-03-25 16:33:15 +01:00
|
|
|
new_width_mutable = term->sixel.max_width;
|
2022-02-03 19:20:41 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
if (unlikely(new_height_mutable > term->sixel.max_height)) {
|
2022-02-03 19:20:41 +01:00
|
|
|
LOG_WARN("maximum image height exceeded, truncating");
|
2024-03-25 16:33:15 +01:00
|
|
|
new_height_mutable = term->sixel.max_height;
|
2021-03-07 12:02:12 +01:00
|
|
|
}
|
2021-03-06 21:37:16 +01:00
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
|
2021-03-07 11:08:01 +01:00
|
|
|
uint32_t *old_data = term->sixel.image.data;
|
|
|
|
|
const int old_width = term->sixel.image.width;
|
|
|
|
|
const int old_height = term->sixel.image.height;
|
2024-03-25 16:33:15 +01:00
|
|
|
const int new_width = new_width_mutable;
|
|
|
|
|
const int new_height = new_height_mutable;
|
2021-03-07 11:08:01 +01:00
|
|
|
|
2023-06-21 11:39:54 +02:00
|
|
|
if (unlikely(old_width == new_width && old_height == new_height))
|
|
|
|
|
return true;
|
|
|
|
|
|
2023-06-20 12:58:35 +02:00
|
|
|
const int sixel_row_height = 6 * term->sixel.pan;
|
2024-03-25 16:33:15 +01:00
|
|
|
const int alloc_new_height =
|
|
|
|
|
(new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height;
|
|
|
|
|
|
2021-03-07 11:08:01 +01:00
|
|
|
xassert(alloc_new_height >= new_height);
|
2023-06-20 12:58:35 +02:00
|
|
|
xassert(alloc_new_height - new_height < sixel_row_height);
|
2021-03-07 11:08:01 +01:00
|
|
|
|
2020-02-22 10:46:35 +01:00
|
|
|
uint32_t *new_data = NULL;
|
2024-10-23 08:35:30 +02:00
|
|
|
const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0];
|
2024-03-25 16:33:15 +01:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* If the image is resized horizontally, or if it's opaque, we
|
|
|
|
|
* need to explicitly initialize the "new" pixels.
|
|
|
|
|
*
|
|
|
|
|
* When the image is *not* resized horizontally, we simply do a
|
|
|
|
|
* realloc(). In this case, there's no need to manually copy the
|
|
|
|
|
* old pixels. We do however need to initialize the new pixels
|
|
|
|
|
* since realloc() returns uninitialized memory.
|
|
|
|
|
*
|
|
|
|
|
* When the image *is* resized horizontally, we need to allocate
|
|
|
|
|
* new memory (when the width changes, the stride changes, and
|
|
|
|
|
* thus we cannot simply realloc())
|
|
|
|
|
*
|
|
|
|
|
* If the default background is transparent, the new pixels need
|
|
|
|
|
* to be initialized to 0x0. We do this by using calloc().
|
|
|
|
|
*
|
|
|
|
|
* If the default background is opaque, then we need to manually
|
|
|
|
|
* initialize the new pixels.
|
|
|
|
|
*/
|
|
|
|
|
const bool initialize_bg =
|
|
|
|
|
!term->sixel.transparent_bg || new_width == old_width;
|
2020-02-22 10:46:35 +01:00
|
|
|
|
|
|
|
|
if (new_width == old_width) {
|
|
|
|
|
/* Width (and thus stride) is the same, so we can simply
|
|
|
|
|
* re-alloc the existing buffer */
|
|
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
new_data = realloc(old_data, new_width * alloc_new_height * sizeof(uint32_t));
|
2020-02-22 10:46:35 +01:00
|
|
|
if (new_data == NULL) {
|
|
|
|
|
LOG_ERRNO("failed to reallocate sixel image buffer");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(new_height > old_height);
|
2020-02-22 11:52:22 +01:00
|
|
|
|
2020-02-22 10:46:35 +01:00
|
|
|
} else {
|
|
|
|
|
/* Width (and thus stride) change - need to allocate a new buffer */
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(new_width > old_width);
|
2024-03-25 16:33:15 +01:00
|
|
|
const size_t pixels = new_width * alloc_new_height;
|
|
|
|
|
|
|
|
|
|
new_data = !initialize_bg
|
|
|
|
|
? xcalloc(pixels, sizeof(uint32_t))
|
|
|
|
|
: xmalloc(pixels * sizeof(uint32_t));
|
2020-02-22 10:46:35 +01:00
|
|
|
|
2020-02-22 11:52:22 +01:00
|
|
|
/* Copy old rows, and initialize new columns to background color */
|
2024-03-25 16:33:15 +01:00
|
|
|
const int row_copy_count = min(old_height, alloc_new_height);
|
|
|
|
|
const uint32_t *end = &new_data[row_copy_count * new_width];
|
2020-02-22 10:46:35 +01:00
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
for (uint32_t *n = new_data, *o = old_data;
|
|
|
|
|
n < end;
|
|
|
|
|
n += new_width, o += old_width)
|
|
|
|
|
{
|
|
|
|
|
memcpy(n, o, old_width * sizeof(uint32_t));
|
|
|
|
|
memset_u32(&n[old_width], bg, new_width - old_width);
|
2020-02-22 11:52:22 +01:00
|
|
|
}
|
2020-02-22 10:46:35 +01:00
|
|
|
free(old_data);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-25 16:33:15 +01:00
|
|
|
if (initialize_bg) {
|
|
|
|
|
memset_u32(&new_data[old_height * new_width],
|
|
|
|
|
bg,
|
|
|
|
|
(alloc_new_height - old_height) * new_width);
|
2020-02-22 11:52:22 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(new_data != NULL);
|
2020-02-22 10:46:35 +01:00
|
|
|
term->sixel.image.data = new_data;
|
|
|
|
|
term->sixel.image.width = new_width;
|
|
|
|
|
term->sixel.image.height = new_height;
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
term->sixel.image.alloc_height = alloc_new_height;
|
2024-03-04 16:40:16 +01:00
|
|
|
term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * new_width + term->sixel.pos.col];
|
2020-02-22 10:46:35 +01:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 21:53:23 +01:00
|
|
|
static void
|
2024-03-04 16:40:16 +01:00
|
|
|
sixel_add_generic(struct terminal *term, uint32_t *data, int stride, uint32_t color,
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
uint8_t sixel)
|
2020-02-21 21:53:23 +01:00
|
|
|
{
|
2023-06-20 12:58:35 +02:00
|
|
|
const int pan = term->sixel.pan;
|
2021-03-06 19:44:26 +01:00
|
|
|
|
2023-06-20 12:58:35 +02:00
|
|
|
for (int i = 0; i < 6; i++, sixel >>= 1) {
|
2021-03-07 14:44:16 +01:00
|
|
|
if (sixel & 1) {
|
2024-03-04 16:40:16 +01:00
|
|
|
for (int r = 0; r < pan; r++, data += stride)
|
2023-06-20 12:58:35 +02:00
|
|
|
*data = color;
|
|
|
|
|
} else
|
2024-03-04 16:40:16 +01:00
|
|
|
data += stride * pan;
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-16 20:16:00 +00:00
|
|
|
xassert(sixel == 0);
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-04 16:40:16 +01:00
|
|
|
static void ALWAYS_INLINE inline
|
|
|
|
|
sixel_add_ar_11(struct terminal *term, uint32_t *data, int stride, uint32_t color,
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
uint8_t sixel)
|
|
|
|
|
{
|
|
|
|
|
xassert(term->sixel.pan == 1);
|
|
|
|
|
|
2023-06-27 16:21:26 +02:00
|
|
|
if (sixel & 0x01)
|
|
|
|
|
*data = color;
|
2024-03-04 16:40:16 +01:00
|
|
|
data += stride;
|
2023-06-27 16:21:26 +02:00
|
|
|
if (sixel & 0x02)
|
|
|
|
|
*data = color;
|
2024-03-04 16:40:16 +01:00
|
|
|
data += stride;
|
2023-06-27 16:21:26 +02:00
|
|
|
if (sixel & 0x04)
|
|
|
|
|
*data = color;
|
2024-03-04 16:40:16 +01:00
|
|
|
data += stride;
|
2023-06-27 16:21:26 +02:00
|
|
|
if (sixel & 0x08)
|
|
|
|
|
*data = color;
|
2024-03-04 16:40:16 +01:00
|
|
|
data += stride;
|
2023-06-27 16:21:26 +02:00
|
|
|
if (sixel & 0x10)
|
|
|
|
|
*data = color;
|
2024-03-04 16:40:16 +01:00
|
|
|
data += stride;
|
2023-06-27 16:21:26 +02:00
|
|
|
if (sixel & 0x20)
|
|
|
|
|
*data = color;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count)
|
2021-03-07 16:30:48 +01:00
|
|
|
{
|
|
|
|
|
int col = term->sixel.pos.col;
|
|
|
|
|
int width = term->sixel.image.width;
|
|
|
|
|
|
2023-06-19 19:06:38 +02:00
|
|
|
count *= term->sixel.pad;
|
|
|
|
|
|
2021-03-07 16:30:48 +01:00
|
|
|
if (unlikely(col + count - 1 >= width)) {
|
2022-02-03 19:20:41 +01:00
|
|
|
resize_horizontally(term, col + count);
|
|
|
|
|
width = term->sixel.image.width;
|
2022-10-13 17:52:34 +02:00
|
|
|
count = min(count, max(width - col, 0));
|
2024-03-11 16:25:45 +01:00
|
|
|
|
|
|
|
|
if (unlikely(count == 0))
|
|
|
|
|
return;
|
2021-03-07 16:30:48 +01:00
|
|
|
}
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
uint32_t color = term->sixel.color;
|
2024-03-04 16:40:16 +01:00
|
|
|
uint32_t *data = term->sixel.image.p;
|
|
|
|
|
uint32_t *end = data + count;
|
|
|
|
|
|
2024-03-11 16:25:45 +01:00
|
|
|
term->sixel.pos.col = col + count;
|
|
|
|
|
term->sixel.image.p = end;
|
|
|
|
|
term->sixel.image.bottom_pixel |= c;
|
|
|
|
|
|
2024-03-04 16:40:16 +01:00
|
|
|
for (; data < end; data++)
|
|
|
|
|
sixel_add_generic(term, data, width, color, c);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ALWAYS_INLINE inline
|
|
|
|
|
sixel_add_one_ar_11(struct terminal *term, uint8_t c)
|
|
|
|
|
{
|
|
|
|
|
xassert(term->sixel.pan == 1);
|
|
|
|
|
xassert(term->sixel.pad == 1);
|
|
|
|
|
|
|
|
|
|
int col = term->sixel.pos.col;
|
|
|
|
|
int width = term->sixel.image.width;
|
|
|
|
|
|
|
|
|
|
if (unlikely(col >= width)) {
|
|
|
|
|
resize_horizontally(term, col + count);
|
|
|
|
|
width = term->sixel.image.width;
|
|
|
|
|
count = min(count, max(width - col, 0));
|
2024-03-07 16:21:06 +01:00
|
|
|
|
|
|
|
|
if (unlikely(count == 0))
|
|
|
|
|
return;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
}
|
|
|
|
|
|
2024-03-11 16:25:45 +01:00
|
|
|
uint32_t *data = term->sixel.image.p;
|
2024-03-04 16:40:16 +01:00
|
|
|
|
|
|
|
|
term->sixel.pos.col += 1;
|
|
|
|
|
term->sixel.image.p += 1;
|
2024-03-11 16:25:45 +01:00
|
|
|
term->sixel.image.bottom_pixel |= c;
|
|
|
|
|
|
|
|
|
|
sixel_add_ar_11(term, data, width, term->sixel.color, c);
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count)
|
|
|
|
|
{
|
|
|
|
|
xassert(term->sixel.pan == 1);
|
|
|
|
|
xassert(term->sixel.pad == 1);
|
|
|
|
|
|
|
|
|
|
int col = term->sixel.pos.col;
|
|
|
|
|
int width = term->sixel.image.width;
|
|
|
|
|
|
|
|
|
|
if (unlikely(col + count - 1 >= width)) {
|
|
|
|
|
resize_horizontally(term, col + count);
|
|
|
|
|
width = term->sixel.image.width;
|
|
|
|
|
count = min(count, max(width - col, 0));
|
2024-03-11 16:25:45 +01:00
|
|
|
|
|
|
|
|
if (unlikely(count == 0))
|
|
|
|
|
return;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
}
|
|
|
|
|
|
2021-09-05 11:08:13 +02:00
|
|
|
uint32_t color = term->sixel.color;
|
2024-03-04 16:40:16 +01:00
|
|
|
uint32_t *data = term->sixel.image.p;
|
|
|
|
|
uint32_t *end = data + count;
|
|
|
|
|
|
2024-03-11 16:25:45 +01:00
|
|
|
term->sixel.pos.col += count;
|
|
|
|
|
term->sixel.image.p = end;
|
|
|
|
|
term->sixel.image.bottom_pixel |= c;
|
|
|
|
|
|
2024-03-04 16:40:16 +01:00
|
|
|
for (; data < end; data++)
|
|
|
|
|
sixel_add_ar_11(term, data, width, color, c);
|
2021-03-07 16:30:48 +01:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:39:11 +02:00
|
|
|
IGNORE_WARNING("-Wpedantic")
|
|
|
|
|
|
2020-02-21 21:53:23 +01:00
|
|
|
static void
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
decsixel_generic(struct terminal *term, uint8_t c)
|
2020-02-21 21:53:23 +01:00
|
|
|
{
|
|
|
|
|
switch (c) {
|
|
|
|
|
case '"':
|
2020-02-22 11:30:30 +01:00
|
|
|
term->sixel.state = SIXEL_DECGRA;
|
|
|
|
|
term->sixel.param = 0;
|
|
|
|
|
term->sixel.param_idx = 0;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '!':
|
|
|
|
|
term->sixel.state = SIXEL_DECGRI;
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.param = 0;
|
|
|
|
|
term->sixel.param_idx = 0;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
term->sixel.repeat_count = 1;
|
2020-02-21 21:53:23 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '#':
|
2020-02-22 11:30:30 +01:00
|
|
|
term->sixel.state = SIXEL_DECGCI;
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.color_idx = 0;
|
|
|
|
|
term->sixel.param = 0;
|
|
|
|
|
term->sixel.param_idx = 0;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '$':
|
2021-03-07 11:51:24 +01:00
|
|
|
if (likely(term->sixel.pos.col <= term->sixel.max_width)) {
|
|
|
|
|
/*
|
2024-02-06 12:36:45 +01:00
|
|
|
* We set, and keep, 'col' outside the image boundary when
|
|
|
|
|
* we've reached the maximum image height, to avoid also
|
2021-03-07 11:51:24 +01:00
|
|
|
* having to check the row vs image height in the common
|
|
|
|
|
* path in sixel_add().
|
|
|
|
|
*/
|
|
|
|
|
term->sixel.pos.col = 0;
|
2024-03-04 16:40:16 +01:00
|
|
|
term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width];
|
2021-03-07 11:51:24 +01:00
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
break;
|
|
|
|
|
|
2024-03-11 16:25:45 +01:00
|
|
|
case '-': /* GNL - Graphical New Line */
|
2023-06-19 19:06:38 +02:00
|
|
|
term->sixel.pos.row += 6 * term->sixel.pan;
|
2020-02-22 10:54:52 +01:00
|
|
|
term->sixel.pos.col = 0;
|
2024-03-11 16:25:45 +01:00
|
|
|
term->sixel.image.bottom_pixel = 0;
|
2024-03-04 16:40:16 +01:00
|
|
|
term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width];
|
2021-03-07 11:22:08 +01:00
|
|
|
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
if (term->sixel.pos.row >= term->sixel.image.alloc_height) {
|
2023-06-19 19:06:38 +02:00
|
|
|
if (!resize_vertically(term, term->sixel.pos.row + 6 * term->sixel.pan))
|
|
|
|
|
term->sixel.pos.col = term->sixel.max_width + 1 * term->sixel.pad;
|
2021-03-07 11:51:24 +01:00
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
break;
|
|
|
|
|
|
2023-06-27 15:39:11 +02:00
|
|
|
case '?' ... '~':
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
sixel_add_many_generic(term, c - 63, 1);
|
2020-02-21 21:53:23 +01:00
|
|
|
break;
|
|
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
case ' ':
|
|
|
|
|
case '\n':
|
|
|
|
|
case '\r':
|
|
|
|
|
break;
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
default:
|
2020-06-09 17:34:04 +02:00
|
|
|
LOG_WARN("invalid sixel character: '%c' at idx=%zu", c, count);
|
2020-02-21 21:53:23 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:39:11 +02:00
|
|
|
UNIGNORE_WARNINGS
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
static void
|
|
|
|
|
decsixel_ar_11(struct terminal *term, uint8_t c)
|
|
|
|
|
{
|
2023-06-27 15:39:11 +02:00
|
|
|
if (likely(c >= '?' && c <= '~'))
|
2024-03-04 16:40:16 +01:00
|
|
|
sixel_add_one_ar_11(term, c - 63);
|
2023-06-27 15:39:11 +02:00
|
|
|
else
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
decsixel_generic(term, c);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 21:53:23 +01:00
|
|
|
static void
|
2020-02-22 11:30:30 +01:00
|
|
|
decgra(struct terminal *term, uint8_t c)
|
2020-02-21 21:53:23 +01:00
|
|
|
{
|
2020-02-22 11:30:30 +01:00
|
|
|
switch (c) {
|
2020-08-23 09:37:51 +02:00
|
|
|
case '0': case '1': case '2': case '3': case '4':
|
|
|
|
|
case '5': case '6': case '7': case '8': case '9':
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.param *= 10;
|
|
|
|
|
term->sixel.param += c - '0';
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
2020-02-22 10:46:35 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
case ';':
|
|
|
|
|
if (term->sixel.param_idx < ALEN(term->sixel.params))
|
|
|
|
|
term->sixel.params[term->sixel.param_idx++] = term->sixel.param;
|
|
|
|
|
term->sixel.param = 0;
|
|
|
|
|
break;
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
default: {
|
|
|
|
|
if (term->sixel.param_idx < ALEN(term->sixel.params))
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.params[term->sixel.param_idx++] = term->sixel.param;
|
|
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
int nparams = term->sixel.param_idx;
|
|
|
|
|
unsigned pan = nparams > 0 ? term->sixel.params[0] : 0;
|
|
|
|
|
unsigned pad = nparams > 1 ? term->sixel.params[1] : 0;
|
|
|
|
|
unsigned ph = nparams > 2 ? term->sixel.params[2] : 0;
|
|
|
|
|
unsigned pv = nparams > 3 ? term->sixel.params[3] : 0;
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
pan = pan > 0 ? pan : 1;
|
|
|
|
|
pad = pad > 0 ? pad : 1;
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2024-04-15 16:07:47 +02:00
|
|
|
if (likely(term->sixel.image.width == 0 &&
|
|
|
|
|
term->sixel.image.height == 0))
|
|
|
|
|
{
|
|
|
|
|
term->sixel.pan = pan;
|
|
|
|
|
term->sixel.pad = pad;
|
|
|
|
|
} else {
|
|
|
|
|
/*
|
|
|
|
|
* Unsure what the VT340 does...
|
|
|
|
|
*
|
|
|
|
|
* We currently do *not* handle changing pan/pad in the
|
|
|
|
|
* middle of a sixel, since that means resizing/stretching
|
|
|
|
|
* the existing image.
|
|
|
|
|
*
|
|
|
|
|
* I'm *guessing* the VT340 simply changes the aspect
|
|
|
|
|
* ratio of all subsequent sixels. But, given the design
|
|
|
|
|
* of our implementation (the entire sixel is written to a
|
|
|
|
|
* single pixman image), we can't easily do that.
|
|
|
|
|
*/
|
|
|
|
|
LOG_WARN("sixel: unsupported: pan/pad changed after printing sixels");
|
|
|
|
|
pan = term->sixel.pan;
|
|
|
|
|
pad = term->sixel.pad;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 19:06:38 +02:00
|
|
|
pv *= pan;
|
|
|
|
|
ph *= pad;
|
|
|
|
|
|
2023-06-20 12:58:35 +02:00
|
|
|
LOG_DBG("pan=%u, pad=%u (aspect ratio = %d:%d), size=%ux%u",
|
|
|
|
|
pan, pad, pan, pad, ph, pv);
|
2020-02-22 10:49:00 +01:00
|
|
|
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
/*
|
|
|
|
|
* RA really only acts as a rectangular erase - it fills the
|
|
|
|
|
* specified area with the sixel background color[^1]. Nothing
|
|
|
|
|
* else. It does *not* affect cursor positioning.
|
|
|
|
|
*
|
|
|
|
|
* This means that if the emitted sixel is *smaller* than the
|
|
|
|
|
* RA, the text cursor will be placed "inside" the RA area.
|
|
|
|
|
*
|
|
|
|
|
* This means it would be more correct to view the RA area as
|
|
|
|
|
* a *separate* sixel image, that is then overlaid with the
|
|
|
|
|
* actual sixel.
|
|
|
|
|
*
|
|
|
|
|
* Still, RA _is_ a hint - the final image is _likely_ going
|
|
|
|
|
* to be this large. And, treating RA as a separate image
|
|
|
|
|
* prevents us from pre-allocating the final sixel image.
|
|
|
|
|
*
|
|
|
|
|
* So we don't. We use the RA as a hint, and pre-allocates the
|
|
|
|
|
* backing image buffer.
|
|
|
|
|
*
|
|
|
|
|
* [^1]: i.e. it's a NOP if the sixel is transparent
|
|
|
|
|
*/
|
2020-02-22 21:03:24 +01:00
|
|
|
if (ph >= term->sixel.image.height && pv >= term->sixel.image.width &&
|
2020-11-23 20:10:55 +01:00
|
|
|
ph <= term->sixel.max_height && pv <= term->sixel.max_width)
|
2020-02-22 21:03:24 +01:00
|
|
|
{
|
sixel: RA region does not affect the text cursor position
The raster attributes is, really, just a way to erase an area. And, if
the sixel is transparent, it's a nop.
The final text cursor position depends, not on our image size (which
is based on RA), but on the final graphical cursor position.
However, we do want to continue using it as a hint of the final image
size, to be able to pre-allocate the backing buffer.
So, here's what we do:
* When trimming trailing transparent rows, only trim the *image*, if
the graphical cursor is positioned on the last sixel row, *and* the
sixel is transparent.
* Opaque sixels aren't trimmed at all, since RA in this acts as an
erase that fills the RA region with the background color.
* The graphical cursor position is always adjusted (i.e. trimmed),
since it affects the text cursor position.
* The text cursor position is now calculated from the graphical cursor
position, instead of the image height.
2024-03-15 15:11:44 +01:00
|
|
|
/*
|
|
|
|
|
* TODO: always resize to a multiple of 6*pan?
|
|
|
|
|
*
|
|
|
|
|
* We're effectively doing that already, except
|
|
|
|
|
* sixel.image.height is set to ph, instead of the
|
|
|
|
|
* allocated height (which is always a multiple of 6*pan).
|
|
|
|
|
*
|
|
|
|
|
* If the user wants to emit a sixel that isn't a multiple
|
|
|
|
|
* of 6 pixels, the bottom sixel rows should all be empty,
|
|
|
|
|
* and (assuming a transparent sixel), trimmed when the
|
|
|
|
|
* final image is generated.
|
|
|
|
|
*/
|
2021-03-06 15:03:47 +01:00
|
|
|
resize(term, ph, pv);
|
2020-02-22 21:03:24 +01:00
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
term->sixel.state = SIXEL_DECSIXEL;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
|
|
|
|
|
/* Update DCS put handler, since pan/pad may have changed */
|
|
|
|
|
term->vt.dcs.put_handler = pan == 1 && pad == 1
|
|
|
|
|
? &sixel_put_ar_11
|
|
|
|
|
: &sixel_put_generic;
|
|
|
|
|
|
|
|
|
|
if (likely(pan == 1 && pad == 1))
|
|
|
|
|
decsixel_ar_11(term, c);
|
|
|
|
|
else
|
|
|
|
|
decsixel_generic(term, c);
|
|
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:39:11 +02:00
|
|
|
IGNORE_WARNING("-Wpedantic")
|
|
|
|
|
|
2020-02-21 21:53:23 +01:00
|
|
|
static void
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
decgri_generic(struct terminal *term, uint8_t c)
|
2020-02-21 21:53:23 +01:00
|
|
|
{
|
2020-02-22 11:30:30 +01:00
|
|
|
switch (c) {
|
2020-08-23 09:37:51 +02:00
|
|
|
case '0': case '1': case '2': case '3': case '4':
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
case '5': case '6': case '7': case '8': case '9': {
|
|
|
|
|
unsigned param = term->sixel.param;
|
|
|
|
|
param *= 10;
|
|
|
|
|
param += c - '0';
|
|
|
|
|
term->sixel.repeat_count = term->sixel.param = param;
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2023-06-27 15:39:11 +02:00
|
|
|
case '?' ... '~': {
|
2023-07-11 00:51:32 +08:00
|
|
|
unsigned count = term->sixel.repeat_count;
|
|
|
|
|
if (unlikely(count == 0)) {
|
|
|
|
|
count = 1;
|
|
|
|
|
}
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
sixel_add_many_generic(term, c - 63, count);
|
2020-02-22 11:30:30 +01:00
|
|
|
term->sixel.state = SIXEL_DECSIXEL;
|
|
|
|
|
break;
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
2022-02-03 19:24:03 +01:00
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
term->sixel.state = SIXEL_DECSIXEL;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
term->vt.dcs.put_handler(term, c);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-27 15:39:11 +02:00
|
|
|
UNIGNORE_WARNINGS
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
static void
|
|
|
|
|
decgri_ar_11(struct terminal *term, uint8_t c)
|
|
|
|
|
{
|
2023-06-27 15:39:11 +02:00
|
|
|
if (likely(c >= '?' && c <= '~')) {
|
2023-07-11 00:51:32 +08:00
|
|
|
unsigned count = term->sixel.repeat_count;
|
|
|
|
|
if (unlikely(count == 0)) {
|
|
|
|
|
count = 1;
|
|
|
|
|
}
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
sixel_add_many_ar_11(term, c - 63, count);
|
|
|
|
|
term->sixel.state = SIXEL_DECSIXEL;
|
2023-06-27 15:39:11 +02:00
|
|
|
} else
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
decgri_generic(term, c);
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
2020-02-22 11:30:30 +01:00
|
|
|
decgci(struct terminal *term, uint8_t c)
|
2020-02-21 21:53:23 +01:00
|
|
|
{
|
2020-02-22 11:30:30 +01:00
|
|
|
switch (c) {
|
2020-08-23 09:37:51 +02:00
|
|
|
case '0': case '1': case '2': case '3': case '4':
|
|
|
|
|
case '5': case '6': case '7': case '8': case '9':
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.param *= 10;
|
|
|
|
|
term->sixel.param += c - '0';
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
case ';':
|
|
|
|
|
if (term->sixel.param_idx < ALEN(term->sixel.params))
|
|
|
|
|
term->sixel.params[term->sixel.param_idx++] = term->sixel.param;
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.param = 0;
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
default: {
|
|
|
|
|
if (term->sixel.param_idx < ALEN(term->sixel.params))
|
|
|
|
|
term->sixel.params[term->sixel.param_idx++] = term->sixel.param;
|
|
|
|
|
|
|
|
|
|
int nparams = term->sixel.param_idx;
|
|
|
|
|
|
2020-02-22 14:02:00 +01:00
|
|
|
if (nparams > 0)
|
|
|
|
|
term->sixel.color_idx = min(term->sixel.params[0], term->sixel.palette_size - 1);
|
2020-02-22 11:30:30 +01:00
|
|
|
|
|
|
|
|
if (nparams > 4) {
|
|
|
|
|
unsigned format = term->sixel.params[1];
|
2020-11-15 19:45:33 +01:00
|
|
|
int c1 = term->sixel.params[2];
|
|
|
|
|
int c2 = term->sixel.params[3];
|
|
|
|
|
int c3 = term->sixel.params[4];
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
switch (format) {
|
|
|
|
|
case 1: { /* HLS */
|
2020-11-15 19:45:33 +01:00
|
|
|
int hue = min(c1, 360);
|
|
|
|
|
int lum = min(c2, 100);
|
|
|
|
|
int sat = min(c3, 100);
|
|
|
|
|
|
|
|
|
|
/*
|
2024-02-06 12:36:45 +01:00
|
|
|
* Sixel's HLS use the following primary color hues:
|
2020-11-15 19:45:33 +01:00
|
|
|
* blue: 0°
|
|
|
|
|
* red: 120°
|
|
|
|
|
* green: 240°
|
|
|
|
|
*
|
2024-02-06 12:36:45 +01:00
|
|
|
* While "standard" HSL uses:
|
2020-11-15 19:45:33 +01:00
|
|
|
* red: 0°
|
|
|
|
|
* green: 120°
|
|
|
|
|
* blue: 240°
|
|
|
|
|
*/
|
|
|
|
|
hue = (hue + 240) % 360;
|
|
|
|
|
|
|
|
|
|
uint32_t rgb = hsl_to_rgb(hue, sat, lum);
|
|
|
|
|
|
2020-02-22 21:18:55 +01:00
|
|
|
LOG_DBG("setting palette #%d = HLS %hhu/%hhu/%hhu (0x%06x)",
|
2020-11-15 19:45:33 +01:00
|
|
|
term->sixel.color_idx, hue, lum, sat, rgb);
|
|
|
|
|
|
2021-03-09 17:23:55 +01:00
|
|
|
term->sixel.palette[term->sixel.color_idx] = 0xffu << 24 | rgb;
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 2: { /* RGB */
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
uint16_t r = 255 * min(c1, 100) / 100;
|
|
|
|
|
uint16_t g = 255 * min(c2, 100) / 100;
|
|
|
|
|
uint16_t b = 255 * min(c3, 100) / 100;
|
2020-02-21 21:53:23 +01:00
|
|
|
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu",
|
2020-02-21 21:53:23 +01:00
|
|
|
term->sixel.color_idx, r, g, b);
|
|
|
|
|
|
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.
The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).
How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.
Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.
Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.
Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.
There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.
Perhaps, in the future, we can enable it by default if:
* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-02-21 11:01:29 +01:00
|
|
|
term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b);
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
2021-09-05 11:08:13 +02:00
|
|
|
} else
|
|
|
|
|
term->sixel.color = term->sixel.palette[term->sixel.color_idx];
|
2020-02-21 21:53:23 +01:00
|
|
|
|
2020-02-22 11:30:30 +01:00
|
|
|
term->sixel.state = SIXEL_DECSIXEL;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
|
|
|
|
|
if (likely(term->sixel.pan == 1 && term->sixel.pad == 1))
|
|
|
|
|
decsixel_ar_11(term, c);
|
|
|
|
|
else
|
|
|
|
|
decsixel_generic(term, c);
|
2020-02-22 11:30:30 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
static void
|
|
|
|
|
sixel_put_generic(struct terminal *term, uint8_t c)
|
|
|
|
|
{
|
|
|
|
|
switch (term->sixel.state) {
|
|
|
|
|
case SIXEL_DECSIXEL: decsixel_generic(term, c); break;
|
|
|
|
|
case SIXEL_DECGRA: decgra(term, c); break;
|
|
|
|
|
case SIXEL_DECGRI: decgri_generic(term, c); break;
|
|
|
|
|
case SIXEL_DECGCI: decgci(term, c); break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void
|
|
|
|
|
sixel_put_ar_11(struct terminal *term, uint8_t c)
|
2020-02-21 21:53:23 +01:00
|
|
|
{
|
|
|
|
|
switch (term->sixel.state) {
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
case SIXEL_DECSIXEL: decsixel_ar_11(term, c); break;
|
2020-02-22 11:30:30 +01:00
|
|
|
case SIXEL_DECGRA: decgra(term, c); break;
|
sixel: special case parsing of images with an aspect ratio of 1:1
Images with an aspect ratio of 1:1 are by far the most common (though
not the default).
It makes a lot of sense, performance wise, to special case
them.
Specifically, the sixel_add() function benefits greatly from this, as
it is the inner most, most heavily executed function when parsing a
sixel image.
sixel_add_many() also benefits, since allows us to drop a
multiplication. Since sixel_add_many() always called first (no other
call sites call sixel_add() directly), this has a noticeable effect on
performance.
Another thing that helps (though not as much), and not specifically
with AR 1:1 images, is special casing DECGRI a bit.
Up until now, it simply updated the current sixel parameter value. The
problem is that the default parameter value is 0. But, a value of 0
should be treated as 1. By adding a special ‘repeat_count’ variable to
the sixel struct, we can initialize it to ‘1’ when we see DECGRI, and
then simply overwrite it as the parameter value gets updated. This
allows us to drop an if..else when emitting the sixel.
2023-06-27 14:25:55 +02:00
|
|
|
case SIXEL_DECGRI: decgri_ar_11(term, c); break;
|
2020-02-22 11:30:30 +01:00
|
|
|
case SIXEL_DECGCI: decgci(term, c); break;
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
2020-02-22 11:30:30 +01:00
|
|
|
|
|
|
|
|
count++;
|
2020-02-21 21:53:23 +01:00
|
|
|
}
|
2020-02-22 14:02:00 +01:00
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_colors_report_current(struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
char reply[24];
|
2021-01-14 21:30:06 +00:00
|
|
|
size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", term->sixel.palette_size);
|
|
|
|
|
term_to_slave(term, reply, n);
|
2020-02-22 21:03:24 +01:00
|
|
|
LOG_DBG("query response for current color count: %u", term->sixel.palette_size);
|
2020-02-22 14:02:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_colors_reset(struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
LOG_DBG("sixel palette size reset to %u", SIXEL_MAX_COLORS);
|
2020-06-10 18:36:54 +02:00
|
|
|
|
|
|
|
|
free(term->sixel.palette);
|
|
|
|
|
term->sixel.palette = NULL;
|
|
|
|
|
|
2020-02-24 18:42:04 +01:00
|
|
|
term->sixel.palette_size = SIXEL_MAX_COLORS;
|
|
|
|
|
sixel_colors_report_current(term);
|
2020-02-22 14:02:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_colors_set(struct terminal *term, unsigned count)
|
|
|
|
|
{
|
|
|
|
|
unsigned new_palette_size = min(max(2, count), SIXEL_MAX_COLORS);
|
|
|
|
|
LOG_DBG("sixel palette size set to %u", new_palette_size);
|
2020-02-24 18:42:04 +01:00
|
|
|
|
sixel: fix double free caused by bad free() in sixel_colors_set()
sixel_color_set() is called when the number of (sixel) color registers
is changed.
It frees the current palette, and changes the “palette size” variable.
Originally, we only had a single palette. This is the one free:d by
sixel_color_set().
Later, we added support for private vs. shared palettes. With this
change, we now have one palette that is “never” free:d (the shared
one), and a private palette that is always free:d after a sixel has
been emitted.
‘sixel.palette’ is a pointer to the palette currently in use, and
should only be accessed **while emitting a sixel**.
This is the pointer sixel_color_set() free:d. So for example, if
‘sixel.palette’ pointed to the shared palette, we’d free the shared
palette. But, we didn’t reset ‘sixel.shared_palette’, causing a double
free later on.
Closes #427
2021-03-30 11:08:03 +02:00
|
|
|
free(term->sixel.private_palette);
|
|
|
|
|
free(term->sixel.shared_palette);
|
|
|
|
|
term->sixel.private_palette = NULL;
|
|
|
|
|
term->sixel.shared_palette = NULL;
|
2020-06-10 18:36:54 +02:00
|
|
|
|
2020-02-24 18:42:04 +01:00
|
|
|
term->sixel.palette_size = new_palette_size;
|
|
|
|
|
sixel_colors_report_current(term);
|
2020-02-22 14:02:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_colors_report_max(struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
char reply[24];
|
2021-01-14 21:30:06 +00:00
|
|
|
size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", SIXEL_MAX_COLORS);
|
|
|
|
|
term_to_slave(term, reply, n);
|
2020-02-22 21:03:24 +01:00
|
|
|
LOG_DBG("query response for max color count: %u", SIXEL_MAX_COLORS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_geometry_report_current(struct terminal *term)
|
|
|
|
|
{
|
|
|
|
|
char reply[64];
|
2021-01-14 21:30:06 +00:00
|
|
|
size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS",
|
2021-01-14 14:41:34 +01:00
|
|
|
min(term->cols * term->cell_width, term->sixel.max_width),
|
|
|
|
|
min(term->rows * term->cell_height, term->sixel.max_height));
|
2021-01-14 21:30:06 +00:00
|
|
|
term_to_slave(term, reply, n);
|
2020-02-22 21:03:24 +01:00
|
|
|
|
|
|
|
|
LOG_DBG("query response for current sixel geometry: %ux%u",
|
2020-11-23 20:10:55 +01:00
|
|
|
term->sixel.max_width, term->sixel.max_height);
|
2020-02-22 21:03:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_geometry_reset(struct terminal *term)
|
|
|
|
|
{
|
2020-11-23 20:10:55 +01:00
|
|
|
LOG_DBG("sixel geometry reset to %ux%u", SIXEL_MAX_WIDTH, SIXEL_MAX_HEIGHT);
|
|
|
|
|
term->sixel.max_width = SIXEL_MAX_WIDTH;
|
|
|
|
|
term->sixel.max_height = SIXEL_MAX_HEIGHT;
|
2020-02-24 18:42:04 +01:00
|
|
|
sixel_geometry_report_current(term);
|
2020-02-22 21:03:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_geometry_set(struct terminal *term, unsigned width, unsigned height)
|
|
|
|
|
{
|
2020-02-24 18:42:04 +01:00
|
|
|
LOG_DBG("sixel geometry set to %ux%u", width, height);
|
2020-02-22 21:03:24 +01:00
|
|
|
term->sixel.max_width = width;
|
|
|
|
|
term->sixel.max_height = height;
|
2020-02-24 18:42:04 +01:00
|
|
|
sixel_geometry_report_current(term);
|
2020-02-22 21:03:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
|
sixel_geometry_report_max(struct terminal *term)
|
|
|
|
|
{
|
2021-03-14 10:58:13 +01:00
|
|
|
unsigned max_width = term->sixel.max_width;
|
|
|
|
|
unsigned max_height = term->sixel.max_height;
|
2020-02-22 21:03:24 +01:00
|
|
|
|
|
|
|
|
char reply[64];
|
2021-01-14 21:30:06 +00:00
|
|
|
size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", max_width, max_height);
|
|
|
|
|
term_to_slave(term, reply, n);
|
2020-02-22 21:03:24 +01:00
|
|
|
|
|
|
|
|
LOG_DBG("query response for max sixel geometry: %ux%u",
|
|
|
|
|
max_width, max_height);
|
2020-02-22 14:02:00 +01:00
|
|
|
}
|