Skip to content

Commit

Permalink
Merge pull request #74 from AnonymouX47/kitty-z-index-blend
Browse files Browse the repository at this point in the history
Update image z-index and blending for `kitty` render style
  • Loading branch information
AnonymouX47 authored Feb 20, 2023
2 parents 5a0dd36 + fe3dd7f commit 135bef8
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 66 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- **(BREAKING!)** Redefined `KittyImage.clear()` ([97eceab]).
- **(BREAKING!)** Changed the valid values for the `z_index` style-specific parameter of the *kitty* render style ([#74]).
- `None` is no longer a valid value.
- The lower bound of the valid value range is now `-(2**31 - 1)`.

### Removed
- The CLI and TUI ([#72]).
Expand All @@ -31,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

[#70]: https://github.com/AnonymouX47/term-image/pull/70
[#72]: https://github.com/AnonymouX47/term-image/pull/72
[#74]: https://github.com/AnonymouX47/term-image/pull/74
[b4533d5]: https://github.com/AnonymouX47/term-image/commit/b4533d5697d41fe0742c2ac895077da3b8d889dc
[97eceab]: https://github.com/AnonymouX47/term-image/commit/b4533d5697d41fe0742c2ac895077da3b8d889dc

Expand Down
86 changes: 48 additions & 38 deletions src/term_image/image/kitty.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class KittyImage(GraphicsImage):
::
[method] [ z [index] ] [ m {0 | 1} ] [ c {0-9} ]
[method] [ z {index} ] [ m {0 | 1} ] [ c {0-9} ]
* ``method``: Render method override.
Expand All @@ -72,17 +72,17 @@ class KittyImage(GraphicsImage):
* ``z``: Image/Text stacking order.
* ``index``: Image z-index. An integer in the **signed 32-bit range**.
* ``index``: Image z-index. An integer in the **signed 32-bit range**
(excluding ``-(2**31)``).
Images drawn in the same location with different z-index values will be
blended if they are semi-transparent. If ``index`` is:
Overlapping images with different z-indexes values will be blended if they are
semi-transparent. If ``index`` is:
* ``>= 0``, the image will be drawn above text.
* ``< 0``, the image will be drawn below text.
* ``< -(2**31)/2``, the image will be drawn below cells with non-default
background color.
* ``z`` without ``index`` is currently only used internally.
* If *absent*, defaults to ``z0`` i.e z-index zero.
* e.g ``z0``, ``z1``, ``z-1``, ``z2147483647``, ``z-2147483648``.
Expand Down Expand Up @@ -113,7 +113,7 @@ class KittyImage(GraphicsImage):
"""

_FORMAT_SPEC: Tuple[re.Pattern] = tuple(
map(re.compile, r"[LW] z(-?\d+)? m[01] c[0-9]".split(" "))
map(re.compile, r"[LW] z-?\d+ m[01] c[0-9]".split(" "))
)
_render_methods: Set[str] = {LINES, WHOLE}
_default_render_method: str = LINES
Expand All @@ -133,12 +133,14 @@ class KittyImage(GraphicsImage):
"z_index": (
0,
(
lambda x: x is None or isinstance(x, int),
"z-index must be `None` or an integer",
lambda x: isinstance(x, int),
"z-index must be an integer",
),
(
lambda x: x is None or -(2**31) <= x < 2**31,
"z-index must be within the 32-bit signed integer range",
# INT32_MIN is reserved for non-native animations
lambda x: -(2**31) < x < 2**31,
"z-index must be within the 32-bit signed integer range "
"(excluding ``-(2**31)``)",
),
),
"mix": (
Expand Down Expand Up @@ -175,7 +177,8 @@ def clear(
Args:
cursor: If ``True``, all images intersecting with the current cursor
position are cleared.
z_index: If given, all images on the given z-index are cleared.
z_index: An integer in the **signed 32-bit range**. If given, all images
on the given z-index are cleared.
now: If ``True`` the images are cleared immediately. Otherwise they're
cleared when next Python's standard output buffer is flushed.
Expand All @@ -192,13 +195,16 @@ def clear(
if not isinstance(cursor, bool):
raise TypeError(f"Invalid type for 'cursor' (got: {type(cursor).__name__})")

_, (type_check, _), (value_check, value_msg) = cls._style_args["z_index"]
if not type_check(z_index):
raise TypeError(
f"Invalid type for 'z_index' (got: {type(z_index).__name__})"
)
if not value_check(z_index):
raise ValueError(value_msg)
if z_index is not None:
if not isinstance(z_index, int):
raise TypeError(
f"Invalid type for 'z_index' (got: {type(z_index).__name__})"
)
if not -(1 << 31) <= z_index < (1 << 31):
raise ValueError(
"z-index must be within the 32-bit signed integer range "
f"(got: {z_index})"
)

if not isinstance(now, bool):
raise TypeError(f"Invalid type for 'now' (got: {type(now).__name__})")
Expand All @@ -225,7 +231,7 @@ def draw(
self,
*args,
method: Optional[str] = None,
z_index: Optional[int] = 0,
z_index: int = 0,
mix: bool = False,
compress: int = 4,
**kwargs,
Expand All @@ -240,14 +246,13 @@ def draw(
effective render method of the instance is used.
z_index: The stacking order of images and text **for non-animations**.
Images drawn in the same location with different z-index values will be
blended if they are semi-transparent. If *z_index* is:
Overlapping images with different z-indexes values will be blended if
they are semi-transparent. If *z_index* is:
* ``>= 0``, the image will be drawn above text.
* ``< 0``, the image will be drawn below text.
* ``< -(2**31)/2``, the image will be drawn below cells with
non-default background color.
* ``None``, internal use only, mentioned for the sake of completeness.
To inter-mixing text with an image, see the *mix* parameter.
Expand Down Expand Up @@ -322,16 +327,16 @@ def is_supported(cls):

@classmethod
def _check_style_format_spec(cls, spec: str, original: str) -> Dict[str, Any]:
parent, (method, (z, index), mix, compress) = cls._get_style_format_spec(
parent, (method, z_index, mix, compress) = cls._get_style_format_spec(
spec, original
)
args = {}
if parent:
args.update(super()._check_style_format_spec(parent, original))
if method:
args["method"] = LINES if method == "L" else WHOLE
if z:
args["z_index"] = index and int(index)
if z_index:
args["z_index"] = int(z_index[1:])
if mix:
args["mix"] = bool(int(mix[-1]))
if compress:
Expand All @@ -343,26 +348,22 @@ def _check_style_format_spec(cls, spec: str, original: str) -> Dict[str, Any]:
def _clear_frame(cls) -> bool:
"""Clears an animation frame on-screen.
| Only used on Kitty <= 0.25.0 because ``z_index=None`` is buggy on these
| Only used on Kitty <= 0.25.0 because ``blend=False`` is buggy on these
versions. Does nothing on any other version or terminal.
| Note that this implementation might do more than required since it clears
all images on screen.
See :py:meth:`~term_image.image.BaseImage._clear_frame` for description.
"""
if cls._KITTY_VERSION and cls._KITTY_VERSION <= (0, 25, 0):
cls.clear()
cls.clear(z_index=-(1 << 31))
return True
return False

def _display_animated(self, *args, **kwargs) -> None:
kwargs["z_index"] = -(1 << 31)
if self._KITTY_VERSION > (0, 25, 0):
kwargs["z_index"] = None
else:
try:
del kwargs["z_index"]
except KeyError:
pass
kwargs["blend"] = False

super()._display_animated(*args, **kwargs)

Expand Down Expand Up @@ -392,10 +393,19 @@ def _render_image(
*,
frame: bool = False,
method: Optional[str] = None,
z_index: Optional[int] = 0,
z_index: int = 0,
mix: bool = False,
compress: int = 4,
blend: bool = True,
) -> str:
"""See :py:meth:`BaseImage._render_image` for the description of the method and
:py:meth:`draw` for parameters not described here.
Args:
blend: If ``False``, the rendered image deletes overlapping/intersecting
images when drawn. Otherwise, the behaviour is dependent on the z-index
and/or the terminal emulator (for images with the same z-index).
"""
# NOTE: It's more efficient to write separate strings to the buffer separately
# than concatenate and write together.

Expand All @@ -422,7 +432,7 @@ def _render_image(
control_data = ControlData(f=format, s=width, c=r_width, z=z_index)
erase = "" if mix else f"{CSI}{r_width}X"
jump_right = f"{CSI}{r_width}C"
if z_index is None:
if not blend:
delete = f"{START}a=d,d=C;{ST}"

if render_method == LINES:
Expand All @@ -434,7 +444,7 @@ def _render_image(
trans = Transmission(
control_data, raw_image.read(bytes_per_line), compress
)
z_index is None and buffer.write(delete)
blend or buffer.write(delete)
for chunk in trans.get_chunks():
buffer.write(chunk)
# Writing spaces clears any text under transparent areas of an image
Expand All @@ -445,7 +455,7 @@ def _render_image(
trans = Transmission(
control_data, raw_image.read(bytes_per_line), compress
)
z_index is None and buffer.write(delete)
blend or buffer.write(delete)
for chunk in trans.get_chunks():
buffer.write(chunk)
buffer.write(erase)
Expand All @@ -456,7 +466,7 @@ def _render_image(
vars(control_data).update(v=height, r=r_height)
return "".join(
(
z_index is None and delete or "",
("" if blend else delete),
Transmission(control_data, raw_image, compress).get_chunked(),
f"{erase}{jump_right}\n" * (r_height - 1),
f"{erase}{jump_right}",
Expand Down
54 changes: 26 additions & 28 deletions tests/test_kitty.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def test_style_format_spec():
"c-1",
"c10",
"c4m1",
"z",
" z1",
"m0 ",
" z1c1 ",
Expand All @@ -96,8 +97,7 @@ def test_style_format_spec():
("z1", {"z_index": 1}),
("z-1", {"z_index": -1}),
(f"z{2**31 - 1}", {"z_index": 2**31 - 1}),
(f"z{-2**31}", {"z_index": -(2**31)}),
("z", {"z_index": None}),
(f"z{-(2**31 - 1)}", {"z_index": -(2**31 - 1)}),
("m0", {}),
("m1", {"mix": True}),
("c4", {}),
Expand Down Expand Up @@ -126,15 +126,15 @@ def test_method(self):
assert KittyImage._check_style_args({"method": value}) == {"method": value}

def test_z_index(self):
for value in (1.0, (), [], "2"):
for value in (None, 1.0, (), [], "2"):
with pytest.raises(TypeError):
KittyImage._check_style_args({"z_index": value})
for value in (-(2**31) - 1, 2**31):
for value in (-(2**31), 2**31):
with pytest.raises(ValueError):
KittyImage._check_style_args({"z_index": value})

assert KittyImage._check_style_args({"z_index": 0}) == {}
for value in (None, 1, -1, -(2**31), 2**31 - 1):
for value in (1, -1, -(2**31 - 1), 2**31 - 1):
assert (
KittyImage._check_style_args({"z_index": value})
== {"z_index": value} # fmt: skip
Expand Down Expand Up @@ -239,9 +239,9 @@ class TestRenderLines:
trans.height = _size
trans.set_render_method(LINES)

def render_image(self, alpha=0.0, *, z=0, m=False, c=4):
def render_image(self, alpha=0.0, *, z=0, m=False, c=4, b=True):
return self.trans._renderer(
self.trans._render_image, alpha, z_index=z, mix=m, compress=c
self.trans._render_image, alpha, z_index=z, mix=m, compress=c, blend=b
)

def _test_image_size(self, image):
Expand Down Expand Up @@ -384,20 +384,12 @@ def test_z_index(self):
assert ("z", "0") in decode_image(line)[0]

# z_index = <int32_t>
for value in (1, -1, -(2**31), 2**31 - 1):
for value in (1, -1, -(2**31 - 1), 2**31 - 1):
render = self.render_image(None, z=value)
assert render == f"{self.trans:1.1#+z{value}}"
for line in render.splitlines():
assert ("z", f"{value}") in decode_image(line)[0]

# z_index = None
render = self.render_image(None, z=None)
assert render == f"{self.trans:1.1#+z}"
for line in render.splitlines():
assert line.startswith(delete)
control_codes = decode_image(line.partition(delete)[2])[0]
assert all(key != "z" for key, value in control_codes)

def test_mix(self):
self.trans.scale = 1.0

Expand Down Expand Up @@ -444,6 +436,13 @@ def test_compress(self):
> len(self.render_image(c=9))
)

def test_blend_false(self):
self.trans.scale = 1.0

render = self.render_image(None, b=False)
for line in render.splitlines():
assert line.startswith(delete)

def test_scaled(self):
# At varying scales
for self.trans.scale in map(lambda x: x / 100, range(10, 101, 10)):
Expand All @@ -467,9 +466,9 @@ class TestRenderWhole:
trans.height = _size
trans.set_render_method(WHOLE)

def render_image(self, alpha=0.0, z=0, m=False, c=4):
def render_image(self, alpha=0.0, z=0, m=False, c=4, b=True):
return self.trans._renderer(
self.trans._render_image, alpha, z_index=z, mix=m, compress=c
self.trans._render_image, alpha, z_index=z, mix=m, compress=c, blend=b
)

def _test_image_size(self, image):
Expand Down Expand Up @@ -599,19 +598,12 @@ def test_z_index(self):
assert ("z", "0") in decode_image(render)[0]

# z_index = <int32_t>
for value in (1, -1, -(2**31), 2**31 - 1):
for value in (1, -1, -(2**31 - 1), 2**31 - 1):
render = self.render_image(None, z=value)
assert render == f"{self.trans:1.1#+z{value}}"
control_codes = decode_image(render)[0]
assert ("z", f"{value}") in control_codes

# z_index = None
render = self.render_image(None, z=None)
assert render == f"{self.trans:1.1#+z}"
assert render.startswith(delete)
control_codes = decode_image(render.partition(delete)[2])[0]
assert all(key != "z" for key, value in control_codes)

def test_mix(self):
self.trans.scale = 1.0

Expand Down Expand Up @@ -650,6 +642,12 @@ def test_compress(self):
assert render == f"{self.trans:1.1#+c{value}}"
assert ("o", "z") in decode_image(render)[0]

def test_blend_false(self):
self.trans.scale = 1.0

render = self.render_image(None, b=False)
assert render.startswith(delete)

def test_scaled(self):
# At varying scales
for self.trans.scale in map(lambda x: x / 100, range(10, 101, 10)):
Expand Down Expand Up @@ -693,7 +691,7 @@ def test_args(self):
with pytest.raises(TypeError, match="'z_index'"):
KittyImage.clear(z_index=value)

for value in (-(2**31) - 1, 2**31):
for value in (-(2**31 + 1), 2**31):
with pytest.raises(ValueError, match="z-index .* range"):
KittyImage.clear(z_index=value)

Expand Down Expand Up @@ -727,7 +725,7 @@ def test_cursor(self):
assert tty_buf.getvalue() == b""

def test_z_index(self):
for value in range(-10, 11):
for value in (-(2**31), *range(-10, 11), 2**31 - 1):
with self.setup_buffer() as (buf, tty_buf):
KittyImage.clear(z_index=value, now=True)
assert buf.getvalue() == b""
Expand Down

0 comments on commit 135bef8

Please sign in to comment.