Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit be4179d

Browse files
authored
Implement support for image/png format in terminal (#15184)
Closes #13287 Now `PIL.Image` and `sympy.init_printing(use_latex="png")` show images. The Kitty graphics protocol is used as [suggested](#10610 (comment)). Alternative implementations: * Putting a [mime renderer extension](https://ipython.readthedocs.io/en/stable/config/shell_mimerenderer.html#mime-renderer-extensions) in `ipython_config.py` is inconvenient, and someone requested a [native](#10610 (comment)) feature * Probing for Kitty graphics support would detect any compatible terminal, but hardcoding terminal executable names avoids startup slowdown * Adding tmux support via Unicode-based Kitty graphics would be very complicated Alternatives protocols: * iTerm2 graphics don't work in Ghostty or st * Sixel requires complicated rasterization and dithering
2 parents c6f48bc + 1ec6bc9 commit be4179d

4 files changed

Lines changed: 87 additions & 2 deletions

File tree

IPython/core/kitty.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Implements https://sw.kovidgoyal.net/kitty/graphics-protocol/
2+
3+
from base64 import b64encode, b64decode
4+
import sys
5+
from typing import Union
6+
7+
def _supports_kitty_graphics() -> bool:
8+
import platform
9+
10+
if platform.system() not in ("Darwin", "Linux"):
11+
return False
12+
13+
if not sys.stdout.isatty():
14+
return False
15+
# Hardcoding process names instead of using
16+
# https://sw.kovidgoyal.net/kitty/graphics-protocol/#querying-support-and-available-transmission-mediums
17+
# to avoid startup slowdown
18+
supported_terminals = {
19+
"ghostty",
20+
"iTerm2",
21+
"kitty",
22+
"konsole",
23+
"warp",
24+
"wayst",
25+
"wezterm-gui",
26+
}
27+
import psutil
28+
29+
process = psutil.Process()
30+
while process := process.parent():
31+
if process.name() in supported_terminals:
32+
return True
33+
return False
34+
35+
36+
supports_kitty_graphics = _supports_kitty_graphics()
37+
38+
39+
def png_to_kitty_ansi(png: bytes) -> str:
40+
if not png.startswith(b"\x89PNG\r\n\x1a\n"):
41+
raise ValueError
42+
# This simplicity resembles
43+
# https://sw.kovidgoyal.net/kitty/graphics-protocol/#a-minimal-example
44+
# but if we need tmux support, we can switch to Unicode like
45+
# https://github.com/hzeller/timg/blob/main/src/kitty-canvas.cc
46+
result = ["\033_Ga=T,f=100,", "m=1;"]
47+
encoded = b64encode(png)
48+
for i in range(0, len(encoded), 4096):
49+
result.append(encoded[i : i + 4096].decode("ascii"))
50+
result.append("\033\\")
51+
result.append("\033_G")
52+
result.append("m=1;")
53+
del result[-2:]
54+
result[-3] = "m=0;"
55+
return "".join(result)
56+
57+
58+
def kitty_png_render(png: Union[bytes, str], _md_dict: object) -> None:
59+
if isinstance(png, str):
60+
png = png_to_kitty_ansi(b64decode(png))
61+
else:
62+
png = png_to_kitty_ansi(png)
63+
print(png)
64+
65+
66+
display_formatter_default_active_types = [
67+
"text/plain",
68+
*(["image/png"] if supports_kitty_graphics else []),
69+
]
70+
71+
terminal_default_mime_renderers = {
72+
"image/png": kitty_png_render,
73+
}

IPython/terminal/interactiveshell.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
from IPython.core.async_helpers import get_asyncio_loop
1010
from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
11+
from IPython.core.kitty import (
12+
display_formatter_default_active_types,
13+
terminal_default_mime_renderers,
14+
)
1115
from IPython.utils.py3compat import input
1216
from IPython.utils.PyColorize import theme_table
1317
from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
@@ -771,7 +775,12 @@ def init_display_formatter(self):
771775
"DisplayFormatter" in config
772776
and "active_types" in config["DisplayFormatter"]
773777
):
774-
self.display_formatter.active_types = ["text/plain"]
778+
self.display_formatter.active_types = display_formatter_default_active_types
779+
if not (
780+
"TerminalInteractiveShell" in config
781+
and "mime_renderers" in config["TerminalInteractiveShell"]
782+
):
783+
self.mime_renderers = terminal_default_mime_renderers
775784

776785
def init_prompt_toolkit_cli(self):
777786
if self.simple_prompt:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"matplotlib-inline>=0.1.6",
3131
'pexpect>4.6; sys_platform != "win32" and sys_platform != "emscripten"',
3232
"prompt_toolkit>=3.0.41,<3.1.0",
33+
"psutil>=7",
3334
"pygments>=2.14.0", # wheel
3435
"stack_data>=0.6.0",
3536
"traitlets>=5.13.0",

tests/test_capture.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ def test_capture_output_no_stderr():
160160

161161
def test_capture_output_no_display():
162162
"""test capture_output(display=False)"""
163-
rich = capture.RichOutput(data=full_data)
163+
data = full_data.copy()
164+
del data["image/png"]
165+
rich = capture.RichOutput(data=data)
164166
with capture.capture_output(display=False) as cap:
165167
print(hello_stdout, end="")
166168
print(hello_stderr, end="", file=sys.stderr)

0 commit comments

Comments
 (0)