Source code for plumbum.cli.image
from __future__ import annotations
__lazy_modules__ = {f"{__spec__.parent}.termsize"}
import sys
import typing
from plumbum import cli, colors
from .termsize import get_terminal_size
if typing.TYPE_CHECKING:
import PIL.Image
class Image:
__slots__ = ["char_ratio", "size"]
def __init__(self, size: tuple[int, int] | None = None, char_ratio: float = 2.45):
self.size = size
self.char_ratio = char_ratio
def best_aspect(
self, orig: tuple[int, int], term: tuple[int, int]
) -> tuple[int, int]:
"""Select a best possible size matching the original aspect ratio.
Size is width, height.
The char_ratio option gives the height of each char with respect
to its width, zero for no effect."""
if not self.char_ratio: # Don't use if char ratio is 0
return term
orig_ratio = orig[0] / orig[1] / self.char_ratio
if int(term[1] / orig_ratio) <= term[0]:
return int(term[1] / orig_ratio), term[1]
return term[0], int(term[0] * orig_ratio)
def show(self, filename: str, double: bool = False) -> None:
"""Display an image on the command line. Can select a size or show in double resolution."""
import PIL.Image
return (
self.show_pil_double(PIL.Image.open(filename))
if double
else self.show_pil(PIL.Image.open(filename))
)
def _init_size(self, im: PIL.Image.Image) -> tuple[int, int]:
"""Return the expected image size"""
if self.size is None:
term_size = get_terminal_size()
return self.best_aspect(im.size, term_size)
return self.size
def show_pil(self, im: PIL.Image.Image) -> None:
"Standard show routine"
size = self._init_size(im)
new_im = im.resize(size).convert("RGB")
for y in range(size[1]):
for x in range(size[0] - 1):
pix = new_im.getpixel((x, y))
sys.stdout.write(
colors.bg.rgb(*pix) + " " # type: ignore[misc]
) # '\u2588'
sys.stdout.write(colors.reset + " \n")
sys.stdout.write(colors.reset + "\n")
sys.stdout.flush()
def show_pil_double(self, im: PIL.Image.Image) -> None:
"Show double resolution on some fonts"
size = self._init_size(im)
size = (size[0], size[1] * 2)
new_im = im.resize(size).convert("RGB")
for y in range(size[1] // 2):
for x in range(size[0] - 1):
pix = new_im.getpixel((x, y * 2))
pixl = new_im.getpixel((x, y * 2 + 1))
sys.stdout.write(
(colors.bg.rgb(*pixl) & colors.fg.rgb(*pix)) + "\u2580" # type: ignore[misc]
)
sys.stdout.write(colors.reset + " \n")
sys.stdout.write(colors.reset + "\n")
sys.stdout.flush()
[docs]
class ShowImageApp(cli.Application):
"Display an image on the terminal"
double = cli.Flag(
["-d", "--double"], help="Double resolution (looks good only with some fonts)"
)
@cli.switch(["-c", "--colors"], cli.Range(1, 4), help="Level of color, 1-4")
def colors_set(self, n: int) -> None: # pylint: disable=no-self-use
colors.use_color = n
size = cli.SwitchAttr(["-s", "--size"], help="Size, should be in the form 100x150")
ratio = cli.SwitchAttr(
["--ratio"], float, default=2.45, help="Aspect ratio of the font"
)
[docs]
@cli.positional(cli.ExistingFile)
def main(self, filename: str) -> None:
size = tuple(map(int, self.size.split("x"))) if self.size else None
Image(size, self.ratio).show(filename, self.double) # type: ignore[arg-type]
__all__ = [
"Image",
"ShowImageApp",
]
def __dir__() -> list[str]:
return list(__all__)
if __name__ == "__main__":
ShowImageApp.run()