├── .gitignore
├── LICENSE
├── README.md
├── pyproject.toml
├── requirements-dev.lock
├── requirements.lock
├── rich_pixels
├── __init__.py
├── _pixel.py
├── _renderer.py
└── py.typed
└── tests
├── .sample_data
├── ascii
│ └── rich_pixels.txt
└── images
│ └── bulbasaur.png
├── __init__.py
├── __snapshots__
└── test_pixel
│ ├── test_ascii_text.svg
│ ├── test_png_image_path.svg
│ └── test_png_image_path_with_halfpixels.svg
└── test_pixel.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | .idea/
161 |
162 | # Mac
163 | .DS_Store
164 | .AppleDouble
165 | .LSOverride
166 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Darren Burns
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rich Pixels
2 |
3 | A Rich-compatible library for writing pixel images and other colourful grids to the
4 | terminal.
5 |
6 |
7 |
8 |
9 |
10 | ## Installation
11 |
12 | Get `rich-pixels` from PyPI.
13 |
14 | ```
15 | pip install rich-pixels
16 | ```
17 |
18 | ## Basic Usage
19 |
20 | ### Images
21 |
22 | #### Image from a file
23 |
24 | You can load an image file from a path using `from_image_path`:
25 |
26 | ```python
27 | from rich_pixels import Pixels
28 | from rich.console import Console
29 |
30 | console = Console()
31 | pixels = Pixels.from_image_path("pokemon/bulbasaur.png")
32 | console.print(pixels)
33 | ```
34 |
35 | #### Pillow image object
36 |
37 | You can create a PIL image object yourself and pass it in to `from_image`.
38 |
39 | ```python
40 | from rich_pixels import Pixels
41 | from rich.console import Console
42 | from PIL import Image
43 |
44 | console = Console()
45 |
46 | with Image.open("path/to/image.png") as image:
47 | pixels = Pixels.from_image(image)
48 |
49 | console.print(pixels)
50 | ```
51 |
52 | Using this approach means you can modify your PIL `Image` beforehard.
53 |
54 | #### ASCII Art
55 |
56 | You can quickly build shapes using a tool like [asciiflow](https://asciiflow.com), and
57 | apply styles the ASCII characters. This provides a quick way of sketching out shapes.
58 |
59 | ```python
60 | from rich_pixels import Pixels
61 | from rich.console import Console
62 | from rich.segment import Segment
63 | from rich.style import Style
64 |
65 | console = Console()
66 |
67 | # Draw your shapes using any character you want
68 | grid = """\
69 | xx xx
70 | ox ox
71 | Ox Ox
72 | xx xx
73 | xxxxxxxxxxxxxxxxx
74 | """
75 |
76 | # Map characters to different characters/styles
77 | mapping = {
78 | "x": Segment(" ", Style.parse("yellow on yellow")),
79 | "o": Segment(" ", Style.parse("on white")),
80 | "O": Segment(" ", Style.parse("on blue")),
81 | }
82 |
83 | pixels = Pixels.from_ascii(grid, mapping)
84 | console.print(pixels)
85 | ```
86 |
87 | ### Using with Textual
88 |
89 | `Pixels` can be integrated into [Textual](https://github.com/Textualize/textual)
90 | applications just like any other Rich renderable.
91 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "rich-pixels"
3 | version = "3.0.1"
4 | description = "A Rich-compatible library for writing pixel images and ASCII art to the terminal."
5 | authors = [
6 | { name = "Darren Burns", email = "darrenb900@gmail.com" }
7 | ]
8 | dependencies = [
9 | "rich>=12.0.0",
10 | "pillow>=10.0.0",
11 | ]
12 | readme = "README.md"
13 | requires-python = ">= 3.8"
14 |
15 | [build-system]
16 | requires = ["hatchling"]
17 | build-backend = "hatchling.build"
18 |
19 | [tool.rye]
20 | managed = true
21 | dev-dependencies = [
22 | "black>=24.1.1",
23 | "pytest>=8.0.0",
24 | "mypy>=1.8.0",
25 | "syrupy>=3.0.6",
26 | "types-pillow>=10.2.0.20240206",
27 | ]
28 |
29 | [tool.hatch.metadata]
30 | allow-direct-references = true
31 |
32 | [tool.hatch.build.targets.wheel]
33 | packages = ["rich_pixels"]
34 |
--------------------------------------------------------------------------------
/requirements-dev.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | black==24.1.1
12 | click==8.1.7
13 | exceptiongroup==1.2.0
14 | iniconfig==2.0.0
15 | markdown-it-py==3.0.0
16 | mdurl==0.1.2
17 | mypy==1.8.0
18 | mypy-extensions==1.0.0
19 | packaging==23.2
20 | pathspec==0.12.1
21 | pillow==10.2.0
22 | platformdirs==4.2.0
23 | pluggy==1.4.0
24 | pygments==2.17.2
25 | pytest==8.0.0
26 | rich==13.7.0
27 | syrupy==4.6.1
28 | tomli==2.0.1
29 | types-pillow==10.2.0.20240206
30 | typing-extensions==4.9.0
31 |
--------------------------------------------------------------------------------
/requirements.lock:
--------------------------------------------------------------------------------
1 | # generated by rye
2 | # use `rye lock` or `rye sync` to update this lockfile
3 | #
4 | # last locked with the following flags:
5 | # pre: false
6 | # features: []
7 | # all-features: false
8 | # with-sources: false
9 |
10 | -e file:.
11 | markdown-it-py==3.0.0
12 | mdurl==0.1.2
13 | pillow==10.2.0
14 | pygments==2.17.2
15 | rich==13.7.0
16 | typing-extensions==4.9.0
17 |
--------------------------------------------------------------------------------
/rich_pixels/__init__.py:
--------------------------------------------------------------------------------
1 | from rich_pixels._pixel import Pixels
2 | from rich_pixels._renderer import Renderer, HalfcellRenderer, FullcellRenderer
3 |
4 | __all__ = [
5 | "Pixels",
6 | "Renderer",
7 | "HalfcellRenderer",
8 | "FullcellRenderer",
9 | ]
10 |
--------------------------------------------------------------------------------
/rich_pixels/_pixel.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pathlib import Path, PurePath
4 | from typing import Iterable, Mapping, Tuple, Union, Optional
5 |
6 | from PIL import Image as PILImageModule
7 | from PIL.Image import Image
8 | from rich.console import Console, ConsoleOptions, RenderResult
9 | from rich.segment import Segment, Segments
10 | from rich.style import Style
11 |
12 | from rich_pixels._renderer import Renderer, HalfcellRenderer, FullcellRenderer
13 |
14 |
15 | class Pixels:
16 | def __init__(self) -> None:
17 | self._segments: Segments | None = None
18 |
19 | @staticmethod
20 | def from_image(
21 | image: Image,
22 | resize: Optional[Tuple[int, int]] = None,
23 | renderer: Renderer | None = None,
24 | ):
25 | """Create a Pixels object from a PIL Image.
26 | Requires 'image' extra dependencies.
27 |
28 | Args:
29 | image: The PIL Image
30 | resize: A tuple of (width, height) to resize the image to.
31 | renderer: The renderer to use. If None, the default half-cell renderer will
32 | be used.
33 | """
34 | segments = Pixels._segments_from_image(image, resize, renderer=renderer)
35 | return Pixels.from_segments(segments)
36 |
37 | @staticmethod
38 | def from_image_path(
39 | path: Union[PurePath, str],
40 | resize: Optional[Tuple[int, int]] = None,
41 | renderer: Renderer | None = None,
42 | ) -> Pixels:
43 | """Create a Pixels object from an image path.
44 | Requires 'image' extra dependencies.
45 |
46 | Args:
47 | path: The path to the image file.
48 | resize: A tuple of (width, height) to resize the image to.
49 | renderer: The renderer to use. If None, the default half-cell renderer will
50 | be used.
51 | """
52 | with PILImageModule.open(Path(path)) as image:
53 | segments = Pixels._segments_from_image(image, resize, renderer=renderer)
54 |
55 | return Pixels.from_segments(segments)
56 |
57 | @staticmethod
58 | def _segments_from_image(
59 | image: Image,
60 | resize: Optional[Tuple[int, int]] = None,
61 | renderer: Renderer | None = None,
62 | ) -> list[Segment]:
63 | if renderer is None:
64 | renderer = HalfcellRenderer()
65 | return renderer.render(image, resize)
66 |
67 | @staticmethod
68 | def from_segments(
69 | segments: Iterable[Segment],
70 | ) -> Pixels:
71 | """Create a Pixels object from an Iterable of Segments instance."""
72 | pixels = Pixels()
73 | pixels._segments = Segments(segments)
74 | return pixels
75 |
76 | @staticmethod
77 | def from_ascii(
78 | grid: str, mapping: Optional[Mapping[str, Segment]] = None
79 | ) -> Pixels:
80 | """
81 | Create a Pixels object from a 2D-grid of ASCII characters.
82 | Each ASCII character can be mapped to a Segment (a character and style combo),
83 | allowing you to add a splash of colour to your grid.
84 |
85 | Args:
86 | grid: A 2D grid of characters (a multi-line string).
87 | mapping: Maps ASCII characters to Segments. Occurrences of a character
88 | will be replaced with the corresponding Segment.
89 | """
90 | if mapping is None:
91 | mapping = {}
92 |
93 | if not grid:
94 | return Pixels.from_segments([])
95 |
96 | segments = []
97 | for character in grid:
98 | segment = mapping.get(character, Segment(character))
99 | segments.append(segment)
100 |
101 | return Pixels.from_segments(segments)
102 |
103 | def __rich_console__(
104 | self, console: Console, options: ConsoleOptions
105 | ) -> RenderResult:
106 | yield self._segments or ""
107 |
108 |
109 | if __name__ == "__main__":
110 | console = Console()
111 | images_path = Path(__file__).parent / "../tests/.sample_data/images"
112 | pixels = Pixels.from_image_path(
113 | images_path / "bulbasaur.png", renderer=FullcellRenderer()
114 | )
115 | console.print("\\[case.1] print with fullpixels renderer")
116 | console.print(pixels)
117 |
118 | pixels = Pixels.from_image_path(
119 | images_path / "bulbasaur.png", renderer=FullcellRenderer(default_color="black")
120 | )
121 | console.print("\\[case.2] print with fullpixels renderer and default_color")
122 | console.print(pixels)
123 |
124 | pixels = Pixels.from_image_path(
125 | images_path / "bulbasaur.png", renderer=HalfcellRenderer()
126 | )
127 | console.print("\\[case.3] print with halfpixels renderer")
128 | console.print(pixels)
129 |
130 | pixels = Pixels.from_image_path(
131 | images_path / "bulbasaur.png", renderer=HalfcellRenderer(default_color="black")
132 | )
133 | console.print("\\[case.4] print with halfpixels renderer and default_color")
134 | console.print(pixels)
135 |
136 | grid = """\
137 | xx xx
138 | ox ox
139 | Ox Ox
140 | xx xx
141 | xxxxxxxxxxxxxxxxx
142 | """
143 |
144 | mapping = {
145 | "x": Segment(" ", Style.parse("yellow on yellow")),
146 | "o": Segment(" ", Style.parse("on white")),
147 | "O": Segment("O", Style.parse("white on blue")),
148 | }
149 | pixels = Pixels.from_ascii(grid, mapping)
150 | console.print("\\[case.5] print ascii")
151 | console.print(pixels)
152 |
--------------------------------------------------------------------------------
/rich_pixels/_renderer.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Callable, Tuple
4 |
5 | from PIL.Image import Image, Resampling
6 | from rich.segment import Segment
7 | from rich.style import Style
8 |
9 | RGBA = Tuple[int, int, int, int]
10 | GetPixel = Callable[[Tuple[int, int]], RGBA]
11 |
12 |
13 | def _get_color(pixel: RGBA, default_color: str | None = None) -> str | None:
14 | r, g, b, a = pixel
15 | return f"rgb({r},{g},{b})" if a > 0 else default_color
16 |
17 |
18 | class Renderer:
19 | """
20 | Base class for renderers.
21 | """
22 |
23 | default_color: str | None
24 | null_style: Style | None
25 |
26 | def __init__(
27 | self,
28 | *,
29 | default_color: str | None = None,
30 | ) -> None:
31 | self.default_color = default_color
32 | self.null_style = (
33 | None if default_color is None else Style.parse(f"on {default_color}")
34 | )
35 |
36 | def render(self, image: Image, resize: tuple[int, int] | None) -> list[Segment]:
37 | """
38 | Render an image to Segments.
39 | """
40 |
41 | rgba_image = image.convert("RGBA")
42 | if resize:
43 | rgba_image = rgba_image.resize(resize, resample=Resampling.NEAREST)
44 |
45 | get_pixel = rgba_image.getpixel
46 | width, height = rgba_image.width, rgba_image.height
47 |
48 | segments = []
49 |
50 | for y in self._get_range(height):
51 | this_row: list[Segment] = []
52 |
53 | this_row += self._render_line(
54 | line_index=y, width=width, get_pixel=get_pixel
55 | )
56 | this_row.append(Segment("\n", self.null_style))
57 |
58 | # TODO: Double-check if this is required - I've forgotten...
59 | if not all(t[1] == "" for t in this_row[:-1]):
60 | segments += this_row
61 |
62 | return segments
63 |
64 | def _get_range(self, height: int) -> range:
65 | """
66 | Get the range of lines to render.
67 | """
68 | raise NotImplementedError
69 |
70 | def _render_line(
71 | self, *, line_index: int, width: int, get_pixel: GetPixel
72 | ) -> list[Segment]:
73 | """
74 | Render a line of pixels.
75 | """
76 | raise NotImplementedError
77 |
78 |
79 | class HalfcellRenderer(Renderer):
80 | """
81 | Render an image to half-height cells.
82 | """
83 |
84 | def render(self, image: Image, resize: tuple[int, int] | None) -> list[Segment]:
85 | # because each row is 2 lines high, so we need to make sure the height is even
86 | target_height = resize[1] if resize else image.size[1]
87 | if target_height % 2 != 0:
88 | target_height += 1
89 |
90 | if image.size[1] != target_height:
91 | resize = (
92 | (resize[0], target_height) if resize else (image.size[0], target_height)
93 | )
94 |
95 | return super().render(image, resize)
96 |
97 | def _get_range(self, height: int) -> range:
98 | return range(0, height, 2)
99 |
100 | def _render_line(
101 | self, *, line_index: int, width: int, get_pixel: GetPixel
102 | ) -> list[Segment]:
103 | line = []
104 | for x in range(width):
105 | line.append(self._render_halfcell(x=x, y=line_index, get_pixel=get_pixel))
106 | return line
107 |
108 | def _render_halfcell(self, *, x: int, y: int, get_pixel: GetPixel) -> Segment:
109 | colors = []
110 |
111 | # get lower pixel, render lower pixel use foreground color, so it must be first
112 | lower_color = _get_color(
113 | get_pixel((x, y + 1)), default_color=self.default_color
114 | )
115 | colors.append(lower_color or "")
116 | # get upper pixel, render upper pixel use background color, it is optional
117 | upper_color = _get_color(get_pixel((x, y)), default_color=self.default_color)
118 | if upper_color:
119 | colors.append(upper_color or "")
120 |
121 | style = Style.parse(" on ".join(colors)) if colors else self.null_style
122 | # use lower halfheight block to render if lower pixel is not transparent
123 | return Segment("▄" if lower_color else " ", style)
124 |
125 |
126 | class FullcellRenderer(Renderer):
127 | """
128 | Render an image to full-height cells.
129 | """
130 |
131 | def _get_range(self, height: int) -> range:
132 | return range(height)
133 |
134 | def _render_line(
135 | self, *, line_index: int, width: int, get_pixel: GetPixel
136 | ) -> list[Segment]:
137 | line = []
138 | for x in range(width):
139 | line.append(self._render_fullcell(x=x, y=line_index, get_pixel=get_pixel))
140 | return line
141 |
142 | def _render_fullcell(self, *, x: int, y: int, get_pixel: GetPixel) -> Segment:
143 | pixel = get_pixel((x, y))
144 | style = (
145 | Style.parse(f"on {_get_color(pixel, default_color=self.default_color)}")
146 | if pixel[3] > 0
147 | else self.null_style
148 | )
149 | return Segment(" ", style)
150 |
--------------------------------------------------------------------------------
/rich_pixels/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrenburns/rich-pixels/a0745ebcc26b966d9dbac5875720364ee5c6a1d3/rich_pixels/py.typed
--------------------------------------------------------------------------------
/tests/.sample_data/ascii/rich_pixels.txt:
--------------------------------------------------------------------------------
1 | ###### ######
2 | # # # #### # # # # # # # ###### # ####
3 | # # # # # # # # # # # # # # #
4 | ###### # # ###### ###### # ## ##### # ####
5 | # # # # # # # # ## # # #
6 | # # # # # # # # # # # # # # #
7 | # # # #### # # # # # # ###### ###### ####
8 |
9 | ================================================================
--------------------------------------------------------------------------------
/tests/.sample_data/images/bulbasaur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrenburns/rich-pixels/a0745ebcc26b966d9dbac5875720364ee5c6a1d3/tests/.sample_data/images/bulbasaur.png
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/darrenburns/rich-pixels/a0745ebcc26b966d9dbac5875720364ee5c6a1d3/tests/__init__.py
--------------------------------------------------------------------------------
/tests/__snapshots__/test_pixel/test_ascii_text.svg:
--------------------------------------------------------------------------------
1 |
91 |
--------------------------------------------------------------------------------
/tests/__snapshots__/test_pixel/test_png_image_path.svg:
--------------------------------------------------------------------------------
1 |
187 |
--------------------------------------------------------------------------------
/tests/test_pixel.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | import pytest
4 | from rich.align import Align
5 | from rich.console import Console
6 | from rich.segment import Segment
7 | from rich.style import Style
8 | from syrupy.extensions.image import SVGImageSnapshotExtension
9 |
10 | from rich_pixels import Pixels, FullcellRenderer
11 |
12 | SAMPLE_DATA_DIR = Path(__file__).parent / ".sample_data/"
13 |
14 |
15 | @pytest.fixture
16 | def svg_snapshot(snapshot):
17 | return snapshot.use_extension(SVGImageSnapshotExtension)
18 |
19 |
20 | def get_console():
21 | console = Console(record=True)
22 | return console
23 |
24 |
25 | def test_png_image_path(svg_snapshot):
26 | console = get_console()
27 | pixels = Pixels.from_image_path(
28 | SAMPLE_DATA_DIR / "images/bulbasaur.png", renderer=FullcellRenderer()
29 | )
30 | console.print(pixels)
31 | svg = console.export_svg()
32 | assert svg == svg_snapshot
33 |
34 |
35 | def test_ascii_text(svg_snapshot):
36 | console = get_console()
37 | ascii = (SAMPLE_DATA_DIR / "ascii/rich_pixels.txt").read_text(encoding="utf-8")
38 | mapping = {
39 | "#": Segment(" ", Style.parse("on #50b332")),
40 | "=": Segment(" ", Style.parse("on #10ada3")),
41 | }
42 | pixels = Pixels.from_ascii(
43 | ascii,
44 | mapping=mapping,
45 | )
46 | console.print(Align.center(pixels))
47 | svg = console.export_svg(title="pixels in the terminal")
48 | assert svg == svg_snapshot
49 |
50 |
51 | def test_png_image_path_with_halfpixels(svg_snapshot):
52 | console = get_console()
53 | pixels = Pixels.from_image_path(
54 | SAMPLE_DATA_DIR / "images/bulbasaur.png",
55 | )
56 | console.print(pixels)
57 | svg = console.export_svg()
58 | assert svg == svg_snapshot
59 |
--------------------------------------------------------------------------------