Skip to content

Commit

Permalink
Merge pull request #37 from AnonymouX47/auto-render-style
Browse files Browse the repository at this point in the history
Automatic render-style selection
  • Loading branch information
AnonymouX47 authored May 5, 2022
2 parents c8b1f5f + 6f0e1bc commit 512d5c0
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 35 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [lib] A common interface to be shared across all image classes ([#34]).
- [lib] `BaseImage`, the baseclass of all image classes ([#34]).
- [lib] `is_supported()` class method for render style support detection ([#34]).
- [lib] Convenience functions for automatic render style selection ([#37]).
- `AutoImage()`, `from_file()` and `from_url()` in `term_image.image`.
- [cli,tui] `--style` command-line option for render style selection ([#37]).
- [lib,cli,tui] Automatic render style selection based on the detected terminal support ([#37]).

### Changed
- [lib] `TermImage` is now a subclass of `BaseImage` ([#34]).
Expand All @@ -17,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

[#34]: https://github.com/AnonymouX47/term-image/pull/34
[#36]: https://github.com/AnonymouX47/term-image/pull/36
[#37]: https://github.com/AnonymouX47/term-image/pull/37


## [0.3.1] - 2022-05-04
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ lint:

# Executing using `python -m` adds CWD to `sys.path`.

test: test-base test-iterator
test: test-base test-iterator test-others

test-text: test-term

Expand All @@ -40,6 +40,9 @@ test-base:
test-iterator:
python -m pytest -v tests/test_image_iterator.py

test-others:
python -m pytest -v tests/test_others.py

test-term:
python -m pytest -v tests/test_term.py

Expand Down
35 changes: 32 additions & 3 deletions docs/source/library/reference/image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,43 @@ Core Library Definitions
========================

.. automodule:: term_image.image
:members:
:imported-members:
:show-inheritance:

The ``term_image.image`` subpackage defines the following:


Convenience Functions
---------------------

These functions automatically detect the best supported render style for the
current terminal.

Since all classes define a common interface, any operation supported by one image
class can be performed on any other image class, except stated otherwise.

.. autofunction:: AutoImage

.. autofunction:: from_file

.. autofunction:: from_url


Image Classes
-------------

.. note:: It's allowed to set properties for :term:`animated` images on non-animated ones, the values are simply ignored.

.. autoclass:: BaseImage
:members:
:show-inheritance:

.. autoclass:: TermImage
:members:
:show-inheritance:

.. autoclass:: ImageIterator
:members:
:show-inheritance:

|
.. _context-manager:
Expand Down
44 changes: 31 additions & 13 deletions term_image/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .config import config_options, store_config
from .exceptions import URLNotFoundError
from .exit_codes import FAILURE, INVALID_ARG, NO_VALID_SOURCE, SUCCESS
from .image import TermImage
from .image import TermImage, _best_style
from .image.common import _ALPHA_THRESHOLD
from .logging import Thread, init_log, log, log_exception
from .logging_multi import Process
Expand Down Expand Up @@ -465,13 +465,14 @@ def update_dict(base: dict, update: dict):
def get_urls(
url_queue: Queue,
images: List[Tuple[str, Image]],
ImageClass: type,
) -> None:
"""Processes URL sources from a/some separate thread(s)"""
source = url_queue.get()
while not interrupted.is_set() and source:
log(f"Getting image from {source!r}", logger, verbose=True)
try:
images.append((basename(source), Image(TermImage.from_url(source))))
images.append((basename(source), Image(ImageClass.from_url(source))))
# Also handles `ConnectionTimeout`
except requests.exceptions.ConnectionError:
log(f"Unable to get {source!r}", logger, _logging.ERROR)
Expand All @@ -489,12 +490,13 @@ def get_urls(
def open_files(
file_queue: Queue,
images: List[Tuple[str, Image]],
ImageClass: type,
) -> None:
source = file_queue.get()
while not interrupted.is_set() and source:
log(f"Opening {source!r}", logger, verbose=True)
try:
images.append((source, Image(TermImage.from_file(source))))
images.append((source, Image(ImageClass.from_file(source))))
except PIL.UnidentifiedImageError as e:
log(str(e), logger, _logging.ERROR)
except OSError as e:
Expand Down Expand Up @@ -564,7 +566,16 @@ def check_arg(
from options/flags, to avoid ambiguity.
For example, `$ term-image [options] -- -image.jpg --image.png`
NOTES:
Render Styles:
auto: The best style is automatically determined based on the detected terminal
support.
term: Uses unicode half blocks with 24-bit color escape codes to represent images
with a density of two pixels per character cell.
Using a terminal-graphics-based style not supported by the active terminal is not
allowed.
FOOTNOTES:
1. The displayed image uses HEIGHT/2 lines, while the number of columns is dependent
on the WIDTH and the FONT RATIO.
The auto sizing is calculated such that the image always fits into the available
Expand Down Expand Up @@ -623,6 +634,15 @@ def check_arg(
f"for proper image scaling (default: {config.font_ratio})"
),
)
general.add_argument(
"--style",
choices=("auto", "term"),
default="auto",
help=(
"Specify the image render style (default: auto) "
'[See "Render Styles" below]'
),
)
mode_options = general.add_mutually_exclusive_group()
mode_options.add_argument(
"--cli",
Expand Down Expand Up @@ -960,13 +980,9 @@ def check_arg(
)
log_options.add_argument(
"--log-level",
metavar="LEVEL",
choices=("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"),
default="WARNING",
help=(
"Set logging level to any of DEBUG, INFO, WARNING, ERROR, CRITICAL "
"(default: WARNING) [6]"
),
help="Specify the logging level for the session (default: WARNING) [6]",
)
log_options.add_argument(
"-q",
Expand Down Expand Up @@ -1059,6 +1075,8 @@ def check_arg(

set_font_ratio(args.font_ratio)

ImageClass = {"auto": _best_style(), "term": TermImage}[args.style]

log("Processing sources", logger, loading=True)

file_images, url_images, dir_images = [], [], []
Expand All @@ -1072,7 +1090,7 @@ def check_arg(
getters = [
Thread(
target=get_urls,
args=(url_queue, url_images),
args=(url_queue, url_images, ImageClass),
name=f"Getter-{n}",
daemon=True,
)
Expand All @@ -1084,7 +1102,7 @@ def check_arg(
file_queue = Queue()
opener = Thread(
target=open_files,
args=(file_queue, file_images),
args=(file_queue, file_images, ImageClass),
name="Opener",
daemon=True,
)
Expand Down Expand Up @@ -1221,13 +1239,13 @@ def check_arg(
)

# Handles `ValueError` and `.exceptions.InvalidSize`
# raised by `TermImage.set_size()`, scaling value checks
# raised by `BaseImage.set_size()`, scaling value checks
# or padding width/height checks.
except ValueError as e:
notify.notify(str(e), level=notify.ERROR)
elif OS_IS_UNIX:
notify.end_loading()
tui.init(args, images, contents)
tui.init(args, images, contents, ImageClass)
else:
log(
"The TUI is not supported on Windows! Try with `--cli`.",
Expand Down
68 changes: 67 additions & 1 deletion term_image/image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,73 @@

from __future__ import annotations

__all__ = ("BaseImage", "TermImage", "ImageIterator")
__all__ = (
"AutoImage",
"from_file",
"from_url",
"BaseImage",
"TermImage",
"ImageIterator",
)

from typing import Optional, Tuple, Union

import PIL

from .common import BaseImage, ImageIterator # noqa:F401
from .term import TermImage # noqa:F401


def AutoImage(
image: PIL.Image.Image,
*,
width: Optional[int] = None,
height: Optional[int] = None,
scale: Tuple[float, float] = (1.0, 1.0),
) -> BaseImage:
"""Convenience function for creating an image instance from a PIL image instance.
Returns:
An instance of a subclass of :py:class:`BaseImage`.
Same arguments and raised exceptions as the :py:class:`BaseImage` class constructor.
"""
return _best_style()(image, width=width, height=height, scale=scale)


def from_file(
filepath: str,
**kwargs: Union[None, int, Tuple[float, float]],
) -> BaseImage:
"""Convenience function for creating an image instance from an image file.
Returns:
An instance of a subclass of :py:class:`BaseImage`.
Same arguments and raised exceptions as :py:meth:`BaseImage.from_file`.
"""
return _best_style().from_file(filepath, **kwargs)


def from_url(
url: str,
**kwargs: Union[None, int, Tuple[float, float]],
) -> BaseImage:
"""Convenience function for creating an image instance from an image URL.
Returns:
An instance of a subclass of :py:class:`BaseImage`.
Same arguments and raised exceptions as :py:meth:`BaseImage.from_url`.
"""
return _best_style().from_url(url, **kwargs)


def _best_style():
for cls in _styles:
if cls.is_supported():
break
return cls


_styles = (TermImage,)
2 changes: 2 additions & 0 deletions term_image/tui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def init(
args: argparse.Namespace,
images: Iterable[Tuple[str, Union[Image, Iterator]]],
contents: dict,
ImageClass: type,
) -> None:
"""Initializes the TUI"""
global is_launched
Expand All @@ -45,6 +46,7 @@ def init(
main.REPEAT = args.repeat
main.RECURSIVE = args.recursive
main.SHOW_HIDDEN = args.all
main.ImageClass = ImageClass
main.loop = Loop(main_widget, palette, unhandled_input=process_input)
main.update_pipe = main.loop.watch_pipe(lambda _: None)

Expand Down
5 changes: 3 additions & 2 deletions term_image/tui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from .. import logging, notify, tui
from ..config import context_keys, expand_key
from ..image import ImageIterator, TermImage
from ..image import ImageIterator
from .keys import (
disable_actions,
display_context_keys,
Expand Down Expand Up @@ -446,7 +446,7 @@ def scan_dir(
yield result, (
entry.name,
(
Image(TermImage.from_file(entry.path))
Image(ImageClass.from_file(entry.path))
if result == IMAGE
else ...
if result == DIR
Expand Down Expand Up @@ -742,6 +742,7 @@ def update_screen():
interrupted: Union[None, Event, mp_Event] = None

# Set from `.tui.init()`
ImageClass: type
displayer: Optional[Generator[None, int, bool]] = None
loop: Optional[tui.Loop] = None
update_pipe: Optional[int] = None
Expand Down
Loading

0 comments on commit 512d5c0

Please sign in to comment.