feat: image rendering — kitty graphics, sixel, and ASCII art backends
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""Image rendering abstraction — kitty graphics, sixel, or ASCII art."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def render_image_kitty(path: str) -> str:
|
||||
"""Return kitty graphics protocol escape sequence for the image."""
|
||||
data = Path(path).read_bytes()
|
||||
b64 = base64.standard_b64encode(data).decode()
|
||||
chunks = [b64[i:i + 4096] for i in range(0, len(b64), 4096)]
|
||||
escape = ""
|
||||
for i, chunk in enumerate(chunks):
|
||||
m = 1 if i < len(chunks) - 1 else 0
|
||||
if i == 0:
|
||||
escape += f"\033_Ga=T,f=100,m={m};{chunk}\033\\"
|
||||
else:
|
||||
escape += f"\033_Gm={m};{chunk}\033\\"
|
||||
return escape
|
||||
|
||||
|
||||
def render_image_sixel(path: str) -> str:
|
||||
"""Convert image to sixel using chafa or img2sixel."""
|
||||
if shutil.which("chafa"):
|
||||
result = subprocess.run(
|
||||
["chafa", "--format=sixel", "--size=80x40", str(path)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
if shutil.which("img2sixel"):
|
||||
result = subprocess.run(
|
||||
["img2sixel", "-w", "640", str(path)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
return f"[Sixel unavailable — install chafa or libsixel. Image: {path}]"
|
||||
|
||||
|
||||
def render_image_ascii(path: str) -> str:
|
||||
"""Convert image to ASCII/Unicode block art using chafa."""
|
||||
if shutil.which("chafa"):
|
||||
result = subprocess.run(
|
||||
["chafa", "--size=60x30", str(path)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
return f"[Image: {path}]"
|
||||
|
||||
|
||||
def render_image(path: str, protocol: str) -> str:
|
||||
"""Render an image using the specified protocol. Returns terminal-ready string."""
|
||||
p = Path(path)
|
||||
|
||||
if p.suffix.lower() == ".svg":
|
||||
try:
|
||||
import cairosvg
|
||||
png_path = p.with_suffix(".png")
|
||||
cairosvg.svg2png(url=str(p), write_to=str(png_path))
|
||||
path = str(png_path)
|
||||
except ImportError:
|
||||
return f"[SVG display requires cairosvg: pip install cairosvg. File: {path}]"
|
||||
|
||||
if protocol == "kitty":
|
||||
return render_image_kitty(path)
|
||||
elif protocol == "sixel":
|
||||
return render_image_sixel(path)
|
||||
else:
|
||||
return render_image_ascii(path)
|
||||
@@ -155,8 +155,10 @@ class KittWorkbenchApp(App):
|
||||
pane = self._get_pane(cmd.pane)
|
||||
if cmd.clear and not isinstance(pane, RichLog):
|
||||
pane.remove_children()
|
||||
# Placeholder — image rendering added in Task 7
|
||||
pane.mount(Static(f"[Image: {cmd.path}]"))
|
||||
|
||||
from kitty_workbench.image_renderer import render_image
|
||||
rendered = render_image(cmd.path, self._image_protocol)
|
||||
pane.mount(Static(rendered, markup=False))
|
||||
|
||||
def _handle_log(self, cmd: LogCmd) -> None:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user