mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-25 01:38:02 +02:00
diff: More work on image support
This commit is contained in:
@@ -3,11 +3,15 @@
|
||||
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from base64 import standard_b64encode
|
||||
|
||||
from kitty.utils import screen_size_function
|
||||
|
||||
from .operations import serialize_gr_command
|
||||
|
||||
try:
|
||||
@@ -15,6 +19,37 @@ try:
|
||||
codecs.lookup(fsenc)
|
||||
except Exception:
|
||||
fsenc = 'utf-8'
|
||||
screen_size = screen_size_function()
|
||||
|
||||
|
||||
class ImageData:
|
||||
|
||||
def __init__(self, fmt, width, height, mode):
|
||||
self.width, self.height, self.fmt, self.mode = width, height, fmt, mode
|
||||
self.transmit_fmt = str(24 if self.mode == 'rgb' else 32)
|
||||
|
||||
|
||||
class OpenFailed(ValueError):
|
||||
|
||||
def __init__(self, path, message):
|
||||
ValueError.__init__(
|
||||
self, 'Failed to open: {} with error: {}'.format(path, message)
|
||||
)
|
||||
self.path = path
|
||||
|
||||
|
||||
def run_imagemagick(path, cmd, keep_stdout=True):
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||
if p.returncode != 0:
|
||||
raise OpenFailed(path, p.stderr.decode('utf-8'))
|
||||
return p
|
||||
|
||||
|
||||
def identify(path):
|
||||
p = run_imagemagick(path, ['identify', '-format', '%m %w %h %A', path])
|
||||
parts = tuple(filter(None, p.stdout.decode('utf-8').split()))
|
||||
mode = 'rgb' if parts[3].lower() == 'false' else 'rgba'
|
||||
return ImageData(parts[0].lower(), int(parts[1]), int(parts[2]), mode)
|
||||
|
||||
|
||||
def can_display_images():
|
||||
@@ -31,6 +66,11 @@ class ImageManager:
|
||||
self.handler = handler
|
||||
self.sent_ids = set()
|
||||
self.filesystem_ok = None
|
||||
self.image_data = {}
|
||||
self.converted_images = {}
|
||||
self.sent_images = {}
|
||||
self.image_id_map = {}
|
||||
self.transmission_status = {}
|
||||
|
||||
def __enter__(self):
|
||||
self.tdir = tempfile.mkdtemp(prefix='kitten-images-')
|
||||
@@ -63,3 +103,61 @@ class ImageManager:
|
||||
if image_id == 1:
|
||||
self.filesystem_ok = payload == 'OK'
|
||||
return
|
||||
|
||||
def send_image(self, path, max_cols=None, max_rows=None, scale_up=False):
|
||||
path = os.path.abspath(path)
|
||||
if path not in self.image_data:
|
||||
self.image_data[path] = identify(path)
|
||||
m = self.image_data[path]
|
||||
ss = screen_size()
|
||||
if max_cols is None:
|
||||
max_cols = ss.cols
|
||||
if max_rows is None:
|
||||
max_rows = ss.rows
|
||||
available_width = max_cols * ss.cell_width
|
||||
available_height = max_rows * ss.cell_height
|
||||
key = path, available_width, available_height
|
||||
skey = self.converted_images.get(key)
|
||||
if skey is None:
|
||||
self.converted_images[key] = skey = self.convert_image(path, available_width, available_height, m, scale_up)
|
||||
final_width, final_height = skey[1:]
|
||||
if final_width == 0:
|
||||
return 0, 0, 0
|
||||
image_id = self.sent_images.get(skey)
|
||||
if image_id is None:
|
||||
image_id = self.sent_image[skey] = self.transmit_image(m, key, *skey)
|
||||
return image_id, skey[0], skey[1]
|
||||
|
||||
def convert_image(self, path, available_width, available_height, image_data, scale_up=False):
|
||||
from kitty.icat import convert
|
||||
try:
|
||||
rgba_path, width, height = convert(path, image_data, available_width, available_height, scale_up, tdir=self.tdir, exc_class=ValueError)
|
||||
except ValueError:
|
||||
rgba_path = None
|
||||
width = height = 0
|
||||
return rgba_path, width, height
|
||||
|
||||
def transmit_image(self, image_data, key, rgba_path, width, height):
|
||||
image_id = len(self.sent_ids) + 1
|
||||
self.image_id_map[key] = image_id
|
||||
self.sent_ids.add(image_id)
|
||||
self.transmission_status[image_id] = 0
|
||||
cmd = {'a': 't', 'f': image_data.transmit_fmt, 's': width, 'v': height, 'i': image_id}
|
||||
if self.filesystem_ok:
|
||||
cmd['t'] = 'f'
|
||||
self.handler.write(serialize_gr_command(
|
||||
cmd, standard_b64encode(rgba_path.encode(fsenc))))
|
||||
else:
|
||||
import zlib
|
||||
data = open(rgba_path, 'rb').read()
|
||||
cmd['S'] = len(data)
|
||||
data = zlib.compress(data)
|
||||
cmd['o'] = 'z'
|
||||
data = standard_b64encode(data)
|
||||
while data:
|
||||
chunk, data = data[:4096], data[4096:]
|
||||
m = 1 if data else 0
|
||||
cmd['m'] = m
|
||||
self.handler.write(serialize_gr_command(cmd, chunk))
|
||||
cmd.clear()
|
||||
return image_id
|
||||
|
||||
@@ -12,12 +12,12 @@ import sys
|
||||
import zlib
|
||||
from base64 import standard_b64encode
|
||||
from collections import namedtuple
|
||||
from math import ceil, floor
|
||||
from math import ceil
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from kitty.cli import parse_args
|
||||
from kitty.constants import appname
|
||||
from kitty.utils import read_with_timeout, screen_size_function
|
||||
from kitty.utils import fit_image, read_with_timeout, screen_size_function
|
||||
|
||||
screen_size = screen_size_function()
|
||||
try:
|
||||
@@ -109,20 +109,6 @@ def write_gr_cmd(cmd, payload=None):
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def fit_image(width, height, pwidth, pheight):
|
||||
if height > pheight:
|
||||
corrf = pheight / float(height)
|
||||
width, height = floor(corrf * width), pheight
|
||||
if width > pwidth:
|
||||
corrf = pwidth / float(width)
|
||||
width, height = pwidth, floor(corrf * height)
|
||||
if height > pheight:
|
||||
corrf = pheight / float(height)
|
||||
width, height = floor(corrf * width), pheight
|
||||
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def calculate_in_cell_x_offset(width, cell_width, align):
|
||||
if align == 'left':
|
||||
return 0
|
||||
@@ -220,7 +206,7 @@ def identify(path):
|
||||
return ImageData(parts[0].lower(), int(parts[1]), int(parts[2]), mode)
|
||||
|
||||
|
||||
def convert(path, m, available_width, available_height, scale_up):
|
||||
def convert(path, m, available_width, available_height, scale_up, tdir=None, err_class=SystemExit):
|
||||
width, height = m.width, m.height
|
||||
cmd = ['convert', '-background', 'none', path]
|
||||
if scale_up:
|
||||
@@ -230,7 +216,7 @@ def convert(path, m, available_width, available_height, scale_up):
|
||||
if width > available_width or height > available_height:
|
||||
width, height = fit_image(width, height, available_width, available_height)
|
||||
cmd += ['-resize', '{}x{}'.format(width, height)]
|
||||
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False) as outfile:
|
||||
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False, dir=tdir) as outfile:
|
||||
run_imagemagick(path, cmd + [outfile.name])
|
||||
# ImageMagick sometimes generated rgba images smaller than the specified
|
||||
# size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
|
||||
@@ -240,7 +226,7 @@ def convert(path, m, available_width, available_height, scale_up):
|
||||
if sz < expected_size:
|
||||
missing = expected_size - sz
|
||||
if missing % (bytes_per_pixel * width) != 0:
|
||||
raise SystemExit('ImageMagick failed to convert {} correctly, it generated {} < {} of data'.format(path, sz, expected_size))
|
||||
raise err_class('ImageMagick failed to convert {} correctly, it generated {} < {} of data'.format(path, sz, expected_size))
|
||||
height -= missing // (bytes_per_pixel * width)
|
||||
|
||||
return outfile.name, width, height
|
||||
|
||||
@@ -83,13 +83,15 @@ def screen_size_function():
|
||||
import array
|
||||
import fcntl
|
||||
import termios
|
||||
Size = namedtuple('Size', 'rows cols width height')
|
||||
Size = namedtuple('Size', 'rows cols width height cell_width cell_height')
|
||||
|
||||
def screen_size():
|
||||
if screen_size.changed:
|
||||
buf = array.array('H', [0, 0, 0, 0])
|
||||
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
|
||||
screen_size.ans = Size(*buf)
|
||||
rows, cols, width, height = tuple(buf)
|
||||
cell_width, cell_height = width // cols, height // rows
|
||||
screen_size.ans = Size(rows, cols, width, height, cell_width, cell_height)
|
||||
screen_size.changed = False
|
||||
return screen_size.ans
|
||||
screen_size.changed = True
|
||||
@@ -99,6 +101,21 @@ def screen_size_function():
|
||||
return ans
|
||||
|
||||
|
||||
def fit_image(width, height, pwidth, pheight):
|
||||
from math import floor
|
||||
if height > pheight:
|
||||
corrf = pheight / float(height)
|
||||
width, height = floor(corrf * width), pheight
|
||||
if width > pwidth:
|
||||
corrf = pwidth / float(width)
|
||||
width, height = pwidth, floor(corrf * height)
|
||||
if height > pheight:
|
||||
corrf = pheight / float(height)
|
||||
width, height = floor(corrf * width), pheight
|
||||
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def set_primary_selection(text):
|
||||
if is_macos or is_wayland:
|
||||
return # There is no primary selection
|
||||
|
||||
Reference in New Issue
Block a user