icat kitten: Add options to mirror images and remove their transparency before displaying them

Fixes #4513
This commit is contained in:
Kovid Goyal 2022-01-14 22:02:35 +05:30
parent dd31ee60f2
commit 4ce6d718c9
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 53 additions and 8 deletions

View File

@ -102,6 +102,9 @@ Detailed list of changes
- Draw the dots for braille characters more evenly spaced at all font sizes (:iss:`4499`) - Draw the dots for braille characters more evenly spaced at all font sizes (:iss:`4499`)
- icat kitten: Add options to mirror images and remove their transparency
before displaying them (:iss:`4513`)
- macOS: Respect the users system-wide global keyboard shortcut preferences - macOS: Respect the users system-wide global keyboard shortcut preferences
(:iss:`4501`) (:iss:`4501`)

View File

@ -19,6 +19,7 @@ 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.guess_mime_type import guess_type
from kitty.rgb import to_color
from kitty.types import run_once from kitty.types import run_once
from kitty.typing import GRT_f, GRT_t from kitty.typing import GRT_f, GRT_t
from kitty.utils import ( from kitty.utils import (
@ -55,6 +56,19 @@ are smaller than the specified area to be scaled up to use as much
of the specified area as possible. of the specified area as possible.
--background
default=none
Specify a background color, this will cause transparent images to be composited on
top of the specified color.
--mirror
default=none
type=choices
choices=none,horizontal,vertical,both
Mirror the image about a horizontal or vertical axis or both.
--clear --clear
type=bool-set type=bool-set
Remove all images currently displayed on the screen. Remove all images currently displayed on the screen.
@ -307,6 +321,9 @@ class ParsedOpts:
place: Optional['Place'] = None place: Optional['Place'] = None
z_index: int = 0 z_index: int = 0
remove_alpha: str = ''
flip: bool = False
flop: bool = False
def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool: def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool:
@ -316,9 +333,10 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
available_height = parsed_opts.place.height * (ss.height // ss.rows) if parsed_opts.place else 10 * m.height available_height = parsed_opts.place.height * (ss.height // ss.rows) if parsed_opts.place else 10 * m.height
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
needs_conversion = needs_scaling or bool(parsed_opts.remove_alpha) or parsed_opts.flip or parsed_opts.flop
file_removed = False file_removed = False
use_number = 0 use_number = 0
if m.fmt == 'png' and not needs_scaling: if m.fmt == 'png' and not needs_conversion:
outfile = path outfile = path
transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f' transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f'
fmt: 'GRT_f' = 100 fmt: 'GRT_f' = 100
@ -328,13 +346,17 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil
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 or args.loop == 0: if len(m) == 1 or args.loop == 0:
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,
remove_alpha=parsed_opts.remove_alpha, flip=parsed_opts.flip, flop=parsed_opts.flop)
else: else:
import struct import struct
use_number = max(1, struct.unpack('@I', os.urandom(4))[0]) use_number = max(1, struct.unpack('@I', os.urandom(4))[0])
with NamedTemporaryFile() as f: with NamedTemporaryFile() as f:
prefix = f.name prefix = f.name
frame_data = render_image(path, prefix, m, available_width, available_height, args.scale_up) frame_data = render_image(
path, prefix, m, available_width, available_height, args.scale_up,
remove_alpha=parsed_opts.remove_alpha, flip=parsed_opts.flip, flop=parsed_opts.flop)
outfile, width, height = frame_data.frames[0].path, frame_data.width, frame_data.height outfile, width, height = frame_data.frames[0].path, frame_data.width, frame_data.height
show( show(
outfile, width, height, parsed_opts.z_index, fmt, transmit_mode, outfile, width, height, parsed_opts.z_index, fmt, transmit_mode,
@ -526,6 +548,13 @@ def main(args: List[str] = sys.argv) -> None:
parsed_opts.z_index = parse_z_index(cli_opts.z_index) parsed_opts.z_index = parse_z_index(cli_opts.z_index)
except Exception: except Exception:
raise SystemExit(f'Not a valid z-index specification: {cli_opts.z_index}') raise SystemExit(f'Not a valid z-index specification: {cli_opts.z_index}')
try:
if cli_opts.background != 'none':
parsed_opts.remove_alpha = to_color(cli_opts.background, validate=True).as_sharp
except ValueError:
raise SystemExit(f'Not a valid color specification: {cli_opts.background}')
parsed_opts.flip = cli_opts.mirror in ('both', 'vertical')
parsed_opts.flop = cli_opts.mirror in ('both', 'horizontal')
if cli_opts.detect_support: if cli_opts.detect_support:
if not detect_support(wait_for=cli_opts.detection_timeout, silent=True): if not detect_support(wait_for=cli_opts.detection_timeout, silent=True):

View File

@ -189,7 +189,9 @@ def render_image(
m: ImageData, m: ImageData,
available_width: int, available_height: int, available_width: int, available_height: int,
scale_up: bool, scale_up: bool,
only_first_frame: bool = False only_first_frame: bool = False,
remove_alpha: str = '',
flip: bool = False, flop: bool = False,
) -> RenderedImage: ) -> RenderedImage:
import tempfile import tempfile
has_multiple_frames = len(m) > 1 has_multiple_frames = len(m) > 1
@ -202,7 +204,15 @@ def render_image(
if exe is None: if exe is None:
raise OSError('Failed to find the ImageMagick convert executable, make sure it is present in PATH') raise OSError('Failed to find the ImageMagick convert executable, make sure it is present in PATH')
cmd = [exe] cmd = [exe]
cmd += ['-background', 'none', '--', path] if remove_alpha:
cmd += ['-background', remove_alpha, '-alpha', 'remove']
else:
cmd += ['-background', 'none']
if flip:
cmd.append('-flip')
if flop:
cmd.append('-flop')
cmd += ['--', path]
if only_first_frame and has_multiple_frames: if only_first_frame and has_multiple_frames:
cmd[-1] += '[0]' cmd[-1] += '[0]'
cmd.append('-auto-orient') cmd.append('-auto-orient')
@ -287,12 +297,15 @@ def render_as_single_image(
path: str, m: ImageData, path: str, m: ImageData,
available_width: int, available_height: int, available_width: int, available_height: int,
scale_up: bool, scale_up: bool,
tdir: Optional[str] = None tdir: Optional[str] = None,
remove_alpha: str = '', flip: bool = False, flop: bool = False,
) -> Tuple[str, int, int]: ) -> Tuple[str, int, int]:
import tempfile import tempfile
fd, output = tempfile.mkstemp(prefix='icat-', suffix=f'.{m.mode}', dir=tdir) fd, output = tempfile.mkstemp(prefix='icat-', suffix=f'.{m.mode}', dir=tdir)
os.close(fd) os.close(fd)
result = render_image(path, output, m, available_width, available_height, scale_up, only_first_frame=True) result = render_image(
path, output, m, available_width, available_height, scale_up,
only_first_frame=True, remove_alpha=remove_alpha, flip=flip, flop=flop)
os.rename(result.frames[0].path, output) os.rename(result.frames[0].path, output)
return output, result.width, result.height return output, result.width, result.height

View File

@ -55,7 +55,7 @@ __all__ = (
'GraphicsCommandType', 'HandlerType', 'AbstractEventLoop', 'AddressFamily', 'Socket', 'CompletedProcess', 'GraphicsCommandType', 'HandlerType', 'AbstractEventLoop', 'AddressFamily', 'Socket', 'CompletedProcess',
'PopenType', 'Protocol', 'TypedDict', 'MarkType', 'ImageManagerType', 'Debug', 'LoopType', 'MouseEvent', 'PopenType', 'Protocol', 'TypedDict', 'MarkType', 'ImageManagerType', 'Debug', 'LoopType', 'MouseEvent',
'TermManagerType', 'BossType', 'ChildType', 'BadLineType', 'MouseButton', 'TermManagerType', 'BossType', 'ChildType', 'BadLineType', 'MouseButton',
'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'SequenceMap', 'CoreTextFont', 'WindowSystemMouseEvent', 'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'AliasMap', 'SequenceMap', 'CoreTextFont', 'WindowSystemMouseEvent',
'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle', 'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle',
'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType' 'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType'
) )