Add support for animations to icat

This commit is contained in:
Kovid Goyal 2021-02-04 14:18:47 +05:30
parent f18a56682f
commit 0f18fedf13
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 93 additions and 8 deletions

View File

@ -5,8 +5,8 @@
import contextlib import contextlib
import os import os
import re import re
import socket
import signal import signal
import socket
import sys import sys
import zlib import zlib
from base64 import standard_b64encode from base64 import standard_b64encode
@ -17,18 +17,18 @@ from typing import (
Dict, Generator, List, NamedTuple, Optional, Pattern, Tuple, Union Dict, Generator, List, NamedTuple, Optional, Pattern, Tuple, Union
) )
from kitty.guess_mime_type import guess_type
from kitty.cli import parse_args from kitty.cli import parse_args
from kitty.cli_stub import IcatCLIOptions from kitty.cli_stub import IcatCLIOptions
from kitty.constants import appname from kitty.constants import appname
from kitty.guess_mime_type import guess_type
from kitty.typing import GRT_f, GRT_t from kitty.typing import GRT_f, GRT_t
from kitty.utils import ( from kitty.utils import (
TTYIO, ScreenSize, ScreenSizeGetter, fit_image, screen_size_function TTYIO, ScreenSize, ScreenSizeGetter, fit_image, screen_size_function
) )
from ..tui.images import ( from ..tui.images import (
ConvertFailed, GraphicsCommand, NoImageMagick, OpenFailed, render_as_single_image, fsenc, ConvertFailed, Dispose, GraphicsCommand, NoImageMagick, OpenFailed,
identify RenderedImage, fsenc, identify, render_as_single_image, render_image
) )
from ..tui.operations import clear_images_on_screen, raw_mode from ..tui.operations import clear_images_on_screen, raw_mode
@ -207,7 +207,8 @@ def show(
fmt: 'GRT_f', fmt: 'GRT_f',
transmit_mode: 'GRT_t' = 't', transmit_mode: 'GRT_t' = 't',
align: str = 'center', align: str = 'center',
place: Optional['Place'] = None place: Optional['Place'] = None,
use_number: int = 0
) -> None: ) -> None:
cmd = GraphicsCommand() cmd = GraphicsCommand()
cmd.a = 'T' cmd.a = 'T'
@ -215,6 +216,9 @@ def show(
cmd.s = width cmd.s = width
cmd.v = height cmd.v = height
cmd.z = zindex cmd.z = zindex
if use_number:
cmd.I = use_number # noqa
cmd.q = 2
if place: if place:
set_cursor_for_place(place, cmd, width, height, align) set_cursor_for_place(place, cmd, width, height, align)
else: else:
@ -232,6 +236,56 @@ def show(
write_chunked(cmd, data) write_chunked(cmd, data)
def show_frames(frame_data: RenderedImage, use_number: int) -> None:
transmit_cmd = GraphicsCommand()
transmit_cmd.a = 'f'
transmit_cmd.I = use_number # noqa
transmit_cmd.q = 2
if can_transfer_with_files:
transmit_cmd.t = 't'
transmit_cmd.f = 24 if frame_data.mode == 'rgb' else 32
def control(frame_number: int = 0, loops: Optional[int] = None, gap: Optional[int] = 0, start_animation: bool = False) -> None:
cmd = GraphicsCommand()
cmd.a = 'a'
cmd.I = use_number # noqa
cmd.r = frame_number
if loops is not None:
cmd.v = loops + 1
if gap is not None:
cmd.z = gap if gap > 0 else -1
if start_animation:
cmd.s = 1
write_gr_cmd(cmd)
anchor_frame = 0
for frame in frame_data.frames:
frame_number = frame.index + 1
if frame.dispose < Dispose.previous:
anchor_frame = frame_number
if frame_number == 1:
control(frame_number, gap=frame.gap, loops=1)
continue
if frame.dispose is Dispose.previous:
if anchor_frame != frame_number:
transmit_cmd.c = anchor_frame
else:
transmit_cmd.c = (frame_number - 1) if frame.needs_blend else 0
transmit_cmd.s = frame.width
transmit_cmd.v = frame.height
transmit_cmd.x = frame.canvas_x
transmit_cmd.y = frame.canvas_y
transmit_cmd.z = frame.gap if frame.gap > 0 else -1
if can_transfer_with_files:
write_gr_cmd(transmit_cmd, standard_b64encode(os.path.abspath(frame.path).encode(fsenc)))
else:
with open(frame.path, 'rb') as f:
data = f.read()
write_chunked(transmit_cmd, data)
control(loops=0, start_animation=True)
def parse_z_index(val: str) -> int: def parse_z_index(val: str) -> int:
origin = 0 origin = 0
if val.startswith('--'): if val.startswith('--'):
@ -254,6 +308,7 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
needs_scaling = m.width > available_width or m.height > available_height needs_scaling = m.width > available_width or m.height > available_height
needs_scaling = needs_scaling or args.scale_up needs_scaling = needs_scaling or args.scale_up
file_removed = False file_removed = False
use_number = 0
if m.fmt == 'png' and not needs_scaling: if m.fmt == 'png' and not needs_scaling:
outfile = path outfile = path
transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f' transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f'
@ -263,8 +318,25 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
else: else:
fmt = 24 if m.mode == 'rgb' else 32 fmt = 24 if m.mode == 'rgb' else 32
transmit_mode = 't' transmit_mode = 't'
if len(m) == 1:
outfile, width, height = render_as_single_image(path, m, available_width, available_height, args.scale_up) outfile, width, height = render_as_single_image(path, m, available_width, available_height, args.scale_up)
show(outfile, width, height, parsed_opts.z_index, fmt, transmit_mode, align=args.align, place=parsed_opts.place) else:
import struct
use_number = max(1, struct.unpack('@I', os.urandom(4))[0])
with NamedTemporaryFile() as f:
prefix = f.name
frame_data = render_image(path, prefix, m, available_width, available_height, args.scale_up)
outfile, width, height = frame_data.frames[0].path, frame_data.width, frame_data.height
show(
outfile, width, height, parsed_opts.z_index, fmt, transmit_mode,
align=args.align, place=parsed_opts.place, use_number=use_number
)
if use_number:
show_frames(frame_data, use_number)
if not can_transfer_with_files:
for fr in frame_data.frames:
with contextlib.suppress(FileNotFoundError):
os.unlink(fr.path)
if not args.place: if not args.place:
print() # ensure cursor is on a new line print() # ensure cursor is on a new line
return file_removed return file_removed

View File

@ -8,6 +8,7 @@ import sys
from base64 import standard_b64encode from base64 import standard_b64encode
from collections import defaultdict, deque from collections import defaultdict, deque
from contextlib import suppress from contextlib import suppress
from enum import IntEnum
from itertools import count from itertools import count
from typing import ( from typing import (
Any, Callable, DefaultDict, Deque, Dict, Iterator, List, Optional, Any, Callable, DefaultDict, Deque, Dict, Iterator, List, Optional,
@ -30,6 +31,13 @@ except Exception:
fsenc = 'utf-8' fsenc = 'utf-8'
class Dispose(IntEnum):
undefined = 0
none = 1
background = 2
previous = 3
class Frame: class Frame:
gap: int # milliseconds gap: int # milliseconds
canvas_width: int canvas_width: int
@ -43,6 +51,7 @@ class Frame:
canvas_y: int canvas_y: int
mode: str mode: str
needs_blend: bool needs_blend: bool
dispose: Dispose
path: str = '' path: str = ''
def __init__(self, identify_data: Union['Frame', Dict[str, str]]): def __init__(self, identify_data: Union['Frame', Dict[str, str]]):
@ -60,6 +69,7 @@ class Frame:
q = identify_data['transparency'].lower() q = identify_data['transparency'].lower()
self.mode = 'rgba' if q in ('blend', 'true') else 'rgb' self.mode = 'rgba' if q in ('blend', 'true') else 'rgb'
self.needs_blend = q == 'blend' self.needs_blend = q == 'blend'
self.dispose = getattr(Dispose, identify_data['dispose'].lower())
def __repr__(self) -> str: def __repr__(self) -> str:
canvas = f'{self.canvas_width}x{self.canvas_height}:{self.canvas_x}+{self.canvas_y}' canvas = f'{self.canvas_width}x{self.canvas_height}:{self.canvas_x}+{self.canvas_y}'
@ -120,7 +130,7 @@ def run_imagemagick(path: str, cmd: Sequence[str], keep_stdout: bool = True) ->
def identify(path: str) -> ImageData: def identify(path: str) -> ImageData:
import json import json
q = '{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h","dpi":"%xx%y"},' q = '{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h","dpi":"%xx%y","dispose":"%D"},'
exe = find_exe('magick') exe = find_exe('magick')
if exe: if exe:
cmd = [exe, 'identify'] cmd = [exe, 'identify']
@ -275,6 +285,9 @@ class GraphicsCommand:
z: int = 0 # z-index z: int = 0 # z-index
d: GRT_d = 'a' # what to delete d: GRT_d = 'a' # what to delete
def __repr__(self) -> str:
return self.serialize().decode('ascii').replace('\033', '^]')
def serialize(self, payload: Union[bytes, str] = b'') -> bytes: def serialize(self, payload: Union[bytes, str] = b'') -> bytes:
items = [] items = []
for k in GraphicsCommand.__annotations__: for k in GraphicsCommand.__annotations__: