Add support for animations to icat
This commit is contained in:
parent
f18a56682f
commit
0f18fedf13
@ -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
|
||||||
|
|||||||
@ -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__:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user