File: unicode.py

package info (click to toggle)
textual-image 0.8.5-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,468 kB
  • sloc: python: 1,851; makefile: 2
file content (97 lines) | stat: -rw-r--r-- 3,632 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""Provides a Rich Renderable to render images as grayscale unicode characters."""

from typing import IO, cast

from PIL import Image as PILImage
from rich.console import Console, ConsoleOptions, RenderResult
from rich.measure import Measurement
from rich.segment import Segment

from textual_image._geometry import ImageSize
from textual_image._pixeldata import PixelData
from textual_image._terminal import get_cell_size
from textual_image._utils import StrOrBytesPath, clamp

_CHARACTERS = [
    "█",  # FULL BLOCK
    "▓",  # DARK SHADE
    "▒",  # MEDIUM SHADE
    "░",  # LIGHT SHADE
    " ",  # SPACE
]

_CHARACTER_LOOKUP = {int(255 / len(_CHARACTERS) * i): c for i, c in enumerate(_CHARACTERS)}


def _map_pixel(pixel_value: int) -> str:
    """Maps a grayscale pixel value to a unicode character.

    Args:
        pixel_value: Pixel value in range 0-255.

    Returns:
        Unicode character resembling the pixel value.
    """
    brightness = clamp(255 - pixel_value, 1, 254)
    index = int(255 / len(_CHARACTER_LOOKUP)) * int(brightness / int(255 / len(_CHARACTER_LOOKUP)))
    return _CHARACTER_LOOKUP[index]


class Image:
    """Rich Renderable to render images as grayscale unicode characters."""

    def __init__(
        self,
        image: StrOrBytesPath | IO[bytes] | PILImage.Image,
        width: int | str | None = None,
        height: int | str | None = None,
    ) -> None:
        """Initialized the `Image`.

        Args:
            image: Path to an image file, a byte stream containing image data, or `PIL.Image.Image` instance with the
                   image data to render.
            width: Width specification to render the image.
                See `textual_image.geometry.ImageSize` for details about possible values.
            height: height specification to render the image.
                See `textual_image.geometry.ImageSize` for details about possible values.
        """
        self._image_data = PixelData(image, mode="grayscale")
        self._render_size = ImageSize(self._image_data.width, self._image_data.height, width, height)

    def cleanup(self) -> None:
        """No-op."""
        pass

    def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
        """Called by Rich to render the `Image`.

        Args:
            console: The `Console` instance to render to.
            options: Options for rendering, i.e. available size information.

        Returns:
            `Segment`s to display.
        """
        terminal_sizes = get_cell_size()

        # We draw one character per pixel. Therefore we just scale to the amount of cells and are done.
        # No need to care about the scaled pixel size.
        width, height = self._render_size.get_cell_size(options.max_width, options.max_height, terminal_sizes)

        for row in self._image_data.scaled(width, height):
            yield Segment("".join(_map_pixel(cast(int, pixel)) for pixel in row) + "\n")

    def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
        """Called by Rich to get the render width without actually rendering the object.

        Args:
            console: The `Console` instance to render to.
            options: Options for rendering, i.e. available size information.

        Returns:
            A `Measurement` containing minimum and maximum widths required to render the object
        """
        terminal_sizes = get_cell_size()
        width, _ = self._render_size.get_cell_size(options.max_width, options.max_height, terminal_sizes)
        return Measurement(width, width)