Skip to content

Commit

Permalink
Merge pull request #51 from AnonymouX47/kitty-updates
Browse files Browse the repository at this point in the history
'kitty' render style improvements
  • Loading branch information
AnonymouX47 committed Jun 17, 2022
2 parents 01e630e + d1306ab commit 2d4db51
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 34 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [cli] `--kz/--kitty-z-index` 'kitty' style-specific option ([#49]).
- [cli] `iterm2` render style choice for the `--style` command-line option ([#50]).
- [cli] `--itn/--iterm2-native` and `--itn-max/--iterm2-native-maxsize` style-specific CL options for 'iterm2' native animation ([#50]).
- [cli] `--kc/--kitty-compress` 'kitty' style-specific option ([#51]).
- [tui] Concurrent/Parallel frame rendering for TUI animations ([#42]).
- [lib,cli,tui] Support for the Kitty terminal graphics protocol ([#39]).
- [lib,cli,tui] Automatic render style selection based on the detected terminal support ([#37]).
Expand Down Expand Up @@ -73,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#47]: https://github.com/AnonymouX47/term-image/pull/47
[#49]: https://github.com/AnonymouX47/term-image/pull/49
[#50]: https://github.com/AnonymouX47/term-image/pull/50
[#51]: https://github.com/AnonymouX47/term-image/pull/51


## [0.3.1] - 2022-05-04
Expand Down
30 changes: 26 additions & 4 deletions term_image/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,7 @@ def check_arg(

kitty_parser = argparse.ArgumentParser(add_help=False)
kitty_options = kitty_parser.add_argument_group(
"Kitty Style Options (CLI-only)",
"Kitty Style Options",
"These options apply only when the 'kitty' render style is used",
)
kitty_options.add_argument(
Expand All @@ -1065,7 +1065,23 @@ def check_arg(
dest="z_index",
default=0,
type=int,
help="Image stacking order (default: 0)",
help=(
"Image stacking order (CLI-only) (default: 0). "
"`>= 0` -> above text, `< 0` -> below text, `< -(2**31)/2` -> "
"below cells with non-default background."
),
)
kitty_options.add_argument(
"--kc",
"--kitty-compress",
metavar="N",
dest="compress",
default=4,
type=int,
help=(
"ZLIB compression level (CLI/TUI) (default: 4). "
"0 -> no compression, 1 -> best speed, 9 -> best compression."
),
)

iterm2_parser = argparse.ArgumentParser(add_help=False)
Expand Down Expand Up @@ -1355,7 +1371,13 @@ def check_arg(
if args.frame_duration:
image.frame_duration = args.frame_duration

if args.style == "iterm2":
if args.style == "kitty":
image.set_render_method(
"lines"
if ImageClass._KITTY_VERSION and image._is_animated
else "whole"
)
elif args.style == "iterm2":
image.set_render_method(
"whole"
if (
Expand Down Expand Up @@ -1400,7 +1422,7 @@ def check_arg(
notify.notify(str(e), level=notify.ERROR)
elif OS_IS_UNIX:
notify.end_loading()
tui.init(args, images, contents, ImageClass)
tui.init(args, style_args, images, contents, ImageClass)
else:
log(
"The TUI is not supported on Windows! Try with `--cli`.",
Expand Down
109 changes: 90 additions & 19 deletions term_image/image/kitty.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
from ..utils import CSI, ESC, ST, lock_tty, query_terminal
from .common import GraphicsImage

FORMAT_SPEC = re.compile(r"([^zm]*)(z(-?\d+)?)?(m[01])?(.*)", re.ASCII)
# Constants for ``KittyImage`` render method
FORMAT_SPEC = re.compile(
r"([^LWzmc]*)([LW])?(z(-?\d+)?)?(m[01])?(c[0-9])?(.*)", re.ASCII
)
# Constants for render methods
LINES = "lines"
WHOLE = "whole"

Expand Down Expand Up @@ -61,7 +63,16 @@ class KittyImage(GraphicsImage):
::
[ z [index] ] [ m {0 | 1} ]
[method] [ z [index] ] [ m {0 | 1} ] [ c {0-9} ]
* ``method``: Render method override.
Can be one of:
* ``L``: **LINES** render method (current frame only, for animated images).
* ``W``: **WHOLE** render method (current frame only, for animated images).
Default: Current effective render method of the image.
* ``z``: Image/Text stacking order.
Expand All @@ -72,8 +83,8 @@ class KittyImage(GraphicsImage):
* ``>= 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 non-default text
background colors.
* ``< -(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 z-index ``0``.
Expand All @@ -90,6 +101,12 @@ class KittyImage(GraphicsImage):
* If *absent*, defaults to ``m0``.
* e.g ``m0``, ``m1``.
* ``c``: ZLIB compression level.
* 1 gives best speed, 9 gives best compression, 0 gives no compression at all.
* This results in a trade-off between render time and transmission size/time.
* If *absent*, defaults to ``c4``.
* e.g ``c0``, ``c9``.
ATTENTION:
Currently supported terminal emulators include:
Expand All @@ -102,6 +119,16 @@ class KittyImage(GraphicsImage):
_default_render_method: str = LINES
_render_method: str = LINES
_style_args = {
"method": (
(
lambda x: isinstance(x, str),
"Render method must be a string",
),
(
lambda x: x in KittyImage._render_methods,
"Unknown render method",
),
),
"z_index": (
(
lambda x: x is None or isinstance(x, int),
Expand All @@ -119,14 +146,29 @@ class KittyImage(GraphicsImage):
),
(lambda _: True, ""),
),
"compress": (
(
lambda x: isinstance(x, int),
"Compression level must be an integer",
),
(
lambda x: 0 <= x <= 9,
"Compression level must be between 0 and 9 (both inclusive)",
),
),
}

_KITTY_VERSION: Tuple[int, int, int] = ()
_KONSOLE_VERSION: Tuple[int, int, int] = ()

# Only defined for the purpose of proper self-documentation
def draw(
self, *args, z_index: Optional[int] = 0, mix: bool = False, **kwargs
self,
*args,
z_index: Optional[int] = 0,
mix: bool = False,
compress: int = 4,
**kwargs,
) -> None:
"""Draws an image to standard output.
Expand All @@ -141,8 +183,8 @@ def draw(
* ``>= 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 non-default text
background colors.
* ``< -(2**31)/2``, the image will be drawn below cells with
non-default background color.
* ``None``, deletes any directly overlapping image.
.. note::
Expand All @@ -159,6 +201,12 @@ def draw(
erased, though text can be inter-mixed with the image after it's
been drawn.
compress: ZLIB compression level.
An integer between 0 and 9: 1 gives best speed, 9 gives best compression,
0 gives no compression at all. This results in a trade-off between render
time and transmission size/time.
kwargs: Keyword arguments passed up the inheritance chain.
See the ``draw()`` method of the parent classes for full details, including the
Expand Down Expand Up @@ -225,7 +273,9 @@ def is_supported(cls):

@classmethod
def _check_style_format_spec(cls, spec: str, original: str) -> Dict[str, Any]:
parent, z, index, mix, invalid = FORMAT_SPEC.fullmatch(spec).groups()
parent, method, z, index, mix, compress, invalid = FORMAT_SPEC.fullmatch(
spec
).groups()
if invalid:
raise _style_error(cls)(
f"Invalid style-specific format specification {original!r}"
Expand All @@ -234,10 +284,14 @@ def _check_style_format_spec(cls, spec: str, original: str) -> Dict[str, Any]:
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 mix:
args["mix"] = bool(int(mix[-1]))
if compress:
args["compress"] = int(compress[-1])

return cls._check_style_args(args)

Expand Down Expand Up @@ -289,8 +343,10 @@ def _render_image(
alpha: Union[None, float, str],
*,
frame: bool = False,
method: Optional[str] = None,
z_index: Optional[int] = 0,
mix: bool = False,
compress: int = 4,
) -> str:
# NOTE: It's more efficient to write separate strings to the buffer separately
# than concatenate and write together.
Expand All @@ -300,6 +356,7 @@ def _render_image(
# Since we use `c` and `r` control data keys, there's no need upscaling the
# image on this end; ensures minimal payload.

render_method = method or self._render_method
r_width, r_height = self.rendered_size
width, height = self._get_minimal_render_size()

Expand All @@ -320,23 +377,29 @@ def _render_image(
delete = f"{START}a=d,d=c;{ST}"
clear = f"{delete}{ESC}7{CSI}{r_width}C{delete}{ESC}8"

if self._render_method == LINES:
if render_method == LINES:
cell_height = height // r_height
bytes_per_line = width * cell_height * (format // 8)
vars(control_data).update(dict(v=cell_height, r=1))

with io.StringIO() as buffer, io.BytesIO(raw_image) as raw_image:
trans = Transmission(control_data, raw_image.read(bytes_per_line))
trans = Transmission(
control_data, raw_image.read(bytes_per_line), compress
)
z_index is None and buffer.write(clear)
buffer.write(trans.get_chunked())
for chunk in trans.get_chunks():
buffer.write(chunk)
# Writing spaces clears any text under transparent areas of an image
for _ in range(r_height - 1):
buffer.write(erase)
buffer.write(jump_right)
buffer.write("\n")
trans = Transmission(control_data, raw_image.read(bytes_per_line))
trans = Transmission(
control_data, raw_image.read(bytes_per_line), compress
)
z_index is None and buffer.write(clear)
buffer.write(trans.get_chunked())
for chunk in trans.get_chunks():
buffer.write(chunk)
buffer.write(erase)
buffer.write(jump_right)

Expand All @@ -346,7 +409,7 @@ def _render_image(
return "".join(
(
z_index is None and clear or "",
Transmission(control_data, raw_image).get_chunked(),
Transmission(control_data, raw_image, compress).get_chunked(),
f"{erase}{jump_right}\n" * (r_height - 1),
f"{erase}{jump_right}",
)
Expand All @@ -360,19 +423,27 @@ class Transmission:
Args:
control: The control data.
payload: The payload.
level: Compression level.
"""

control: ControlData
payload: bytes

# From tests with a few images, from anything beyond 4, the decrease in size
# doesn't seem to be worth the increase in compression time in most cases.
# Might change if proven otherwise.
level: int = 4

def __post_init__(self):
self._compressed = False
if self.control.o == o.ZLIB:
if self.level:
self.compress()
else:
self.control.o = None

def compress(self):
if self.control.t == t.DIRECT and not self._compressed:
self.payload = compress(self.payload)
if self.control.t == t.DIRECT and not self._compressed and self.level:
self.payload = compress(self.payload, self.level)
self.control.o = o.ZLIB
self._compressed = True

Expand Down Expand Up @@ -464,7 +535,7 @@ class ControlData:
s: Optional[int] = None # image width
v: Optional[int] = None # image height
z: Optional[int] = z.IN_FRONT # z-index
o: Optional[str] = o.ZLIB # compression
o: Optional[str] = None # compression
C: Optional[int] = C.STAY # cursor movement policy

# # Image display size in columns and rows/lines
Expand Down
17 changes: 12 additions & 5 deletions term_image/tui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging as _logging
import os
from pathlib import Path
from typing import Iterable, Iterator, Tuple, Union
from typing import Any, Dict, Iterable, Iterator, Tuple, Union

import urwid

Expand All @@ -20,6 +20,7 @@

def init(
args: argparse.Namespace,
style_args: Dict[str, Any],
images: Iterable[Tuple[str, Union[Image, Iterator]]],
contents: dict,
ImageClass: type,
Expand Down Expand Up @@ -60,11 +61,17 @@ def init(
Image._ti_alpha = (
"#" if args.no_alpha else "#" + (args.alpha_bg or f"{args.alpha:f}"[1:])
)
# KONSOLE does NOT blend images with the same z-index, hence is better off without
# `z_index=None`
if args.style == "kitty" and ImageClass._KONSOLE_VERSION:

# Kitty blends images with the same z-index, hence the `z_index=None`, which is
# pretty glitchy for animations with WHOLE method, hence the change to LINES
if args.style == "kitty":
if ImageClass._KITTY_VERSION:
render.anim_style_specs["kitty"] = "+L"
for name in ("anim", "grid", "image"):
del getattr(render, f"{name}_style_specs")["kitty"]
specs = getattr(render, f"{name}_style_specs")
if ImageClass._KITTY_VERSION:
specs["kitty"] += "z"
specs["kitty"] += f"c{style_args['compress']}"
Image._ti_grid_style_spec = render.grid_style_specs.get(args.style, "")

# daemon, to avoid having to check if the main process has been interrupted
Expand Down
7 changes: 4 additions & 3 deletions term_image/tui/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,10 @@ def render_images(
grid_render_queue = Queue()
image_render_queue = Queue()

anim_style_specs = {"kitty": "+z", "iterm2": "+Wm1"}
grid_style_specs = {"kitty": "+z"}
image_style_specs = {"kitty": "+z", "iterm2": "+W"}
# Updated from `.tui.init()`
anim_style_specs = {"kitty": "+W", "iterm2": "+Wm1"}
grid_style_specs = {"kitty": "+L"}
image_style_specs = {"kitty": "+W", "iterm2": "+W"}

# Set from `.tui.init()`
# # Corresponsing to command-line args
Expand Down
Loading

0 comments on commit 2d4db51

Please sign in to comment.