Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto font ratio #45

Merged
merged 11 commits into from
May 26, 2022
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [lib] Support for multiple render methods per render style via `BaseImage.set_render_method()` ([#39]).
- [lib] Non-linear image iteration via `ImageIterator.seek()` ([#42]).
- [lib] Image category subclasses (of `BaseImage`), `TextImage` and `GraphicsImage` ([#44]).
- [lib] Automatic font ratio computation ([#45]).
- [lib] `term_image.FontRatio` enumeration class ([#45]).
- [cli] `--style` command-line option for render style selection ([#37]).
- [cli] `kitty` render style choice for the `--style` CL option ([#39]).
- [cli] `--force-style` to bypass render style support checks ([#44]).
- [cli] `--auto-font-ratio` for automatic font ratio determination ([#45]).
- [tui] Concurrent/Parallel frame rendering for TUI animations ([#42]).
- [cli,tui] `--style` command-line option for render style selection ([#37]).
- [lib,cli,tui] Support for the Kitty terminal graphics protocol ([#39]).
- [lib,cli,tui] Automatic render style selection based on the detected terminal support ([#37]).

Expand All @@ -38,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [lib] Improved `repr()` of image instances ([#38]).
- [lib] Direct baseclass of `TermImage` to `TextImage` ([#44]).
- [cli] `-S` from `--scroll` to `--style` ([#44]).
- [cli,tui] Changed default value of `font ratio` config option to `null` ([#45]).

[#34]: https://github.com/AnonymouX47/term-image/pull/34
[#36]: https://github.com/AnonymouX47/term-image/pull/36
Expand All @@ -48,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#42]: https://github.com/AnonymouX47/term-image/pull/42
[#43]: https://github.com/AnonymouX47/term-image/pull/43
[#44]: https://github.com/AnonymouX47/term-image/pull/44
[#45]: https://github.com/AnonymouX47/term-image/pull/45


## [0.3.1] - 2022-05-04
Expand Down
54 changes: 49 additions & 5 deletions docs/source/library/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@ Reference
exceptions
utils

Top-Level Functions
-------------------
Top-Level Definitions
---------------------

.. autofunction:: term_image.get_font_ratio
.. autoclass:: term_image.FontRatio
:show-inheritance:

.. autoattribute:: AUTO
:annotation:

.. autoattribute:: FULL_AUTO
:annotation:

.. autofunction:: term_image.set_font_ratio

|
.. autofunction:: term_image.get_font_ratio

.. _format-spec:
|


.. _render-styles:
Expand Down Expand Up @@ -54,6 +61,43 @@ Classes for render styles in this category are subclasses of :py:class:`Graphics
- :py:class:`KittyImage <term_image.image.KittyImage>`.


.. _auto-font-ratio:

Auto Font Ratio
---------------

When using **auto font ratio** (in either mode), it's important to note that some (not all) terminal emulators (e.g VTE-based ones) might have to be queried, which involves:

1. Clearing all unread input from the active terminal
2. Writing to the active terminal
3. Reading from the active terminal

For this communication to be successful, it must not be interrupted.

About #1
If this isn't a concern i.e the program will never expect any useful input, **particularly while an image's size is being set or when an image with** :ref:`unset size <unset-size>` **is being rendered**, then using ``FULL_AUTO`` mode is OK.

Otherwise i.e if the program will be expecting input:

* Use ``AUTO`` mode.
* Use :py:func:`utils.read_input() <term_image.utils.read_input>` (simply calling it without any argument is enough) to read all unread input (**without blocking**) before calling :py:func:`set_font_ratio() <term_image.set_font_ratio>`.

About #2 and #3
If the program includes any other function that could write to the terminal OR especially, read from the terminal or modify it's attributes, while a query is in progress, decorate it with :py:func:`utils.lock_input <term_image.utils.lock_input>` to ensure it doesn't interfere.

For example, the TUI included in this package (i.e ``term_image``) uses `urwid <https://urwid.org>`_ which reads from the terminal using ``urwid.raw_display.Screen.get_available_raw_input()``. To prevent this method from interfering with terminal queries, it is wrapped thus::

urwid.raw_display.Screen.get_available_raw_input = lock_input(
urwid.raw_display.Screen.get_available_raw_input
)

Also, if the :term:`active terminal` is not the controlling terminal of the process using this library (e.g output is redirected to another terminal), ensure no process that can interfere with a query (e.g a shell) is currently running in the active terminal.

For instance, such a process can be temporarily put to sleep.


.. _format-spec:

Image Format Specification
--------------------------

Expand Down
6 changes: 4 additions & 2 deletions docs/source/viewer/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ checkers
font ratio
The :term:`font ratio`. [\*]

* Type: float
* Valid values: x > ``0.0``
* Type: null or float
* Valid values: ``null`` or x > ``0.0``

If ``null``, the ratio is determined from the :term:`active terminal` such that the proportion of any image is always correct. If this is not supported in the :term:`active terminal` or on the platform, ``0.5`` is used instead.

getters
Number of threads for downloading images from URL sources.
Expand Down
20 changes: 20 additions & 0 deletions docs/source/viewer/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ See :ref:`render-styles`.
The ``--force-style`` command-line option can be used to bypass style support checks and force the usage of any style whether it's supported or not.


Font Ratio
----------

The :term:`font ratio` is taken into consideration when setting image sizes for **text-based** render styles, in order for images drawn to the terminal to have correct proportion.

| This value is determined by the :ref:`config option <font-ratio-config>` ``font ratio`` OR either of the command-line options ``-F | --font-ratio`` and ``--auto-font-ratio``.
| The command-line options are mutually exclusive and override the config option.

| By default (i.e without changing the config option value or specifying the command-line option), ``term-image`` tries to determine the value from the :term:`active terminal` which works on most mordern terminal emulators (currently supported on UNIX-like platforms only).
| This is probably the best choice, except the terminal emulator or platform doesn't support this feature.

| If ``term-image`` is unable to determine this value automatically, it falls back to ``0.5``, which is a reasonable value in most cases.
| In case *auto* font ratio is not supported and the fallback value does not give expected results, a different value can be specified using the config or command-line option.

.. attention::
If using *auto* font ratio and the :term:`active terminal` is not the controlling terminal of the `term-image` process (e.g output is redirected to another terminal), ensure no process that might read input (e.g a shell) is currently running in the active terminal, as such a process might interfere with determining the font ratio on some terminal emulators (e.g VTE-based ones).

For instance, the ``sleep`` command can be executed if a shell is currently running in the active terminal.


Notifications
-------------

Expand Down
108 changes: 76 additions & 32 deletions term_image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,51 +19,95 @@

from __future__ import annotations

__all__ = ("set_font_ratio", "get_font_ratio")
__all__ = ("FontRatio", "set_font_ratio", "get_font_ratio")
__author__ = "AnonymouX47"

from enum import Enum, auto
from operator import truediv
from typing import Union

from .exceptions import TermImageException
from .utils import get_cell_size

version_info = (0, 4, 0, "dev0")
__version__ = ".".join(map(str, version_info))


def get_font_ratio() -> float:
"""Returns the set libray-wide :term:`font ratio`."""
return _font_ratio
"""Returns the libray-wide :term:`font ratio`.

See :py:func:`set_font_ratio`.
"""
# `(1, 2)` is a fallback in case the terminal doesn't respond in time
return _font_ratio or truediv(*(get_cell_size() or (1, 2)))

def set_font_ratio(ratio: float) -> None:

def set_font_ratio(ratio: Union[float, FontRatio]) -> None:
"""Sets the library-wide :term:`font ratio`.

Args:
ratio: The aspect ratio (i.e `width / height`) of a character cell in the
terminal emulator.

This value is taken into consideration when setting image sizes in order for images
drawn to the terminal to have correct proportion.

If you can't determine this value from your terminal's configuration,
you might have to try different values till you get a good fit.
Normally, this value should be between 0 and 1, but not too close to either.
ratio: Can be one of the following values.

* A positive ``float``: a fixed aspect ratio of a character cell in the
terminal emulator.
* :py:attr:`FontRatio.AUTO`: the ratio is immediately determined from the
:term:`active terminal`.
* :py:attr:`FontRatio.FULL_AUTO`: the ratio is determined from the
:term:`active terminal` whenever :py:func:`get_font_ratio` is called,
though with some caching involved, such that the ratio is re-determined
only if the terminal size changes.

Raises:
TypeError: An argument is of an inappropriate type.
ValueError: An argument is of an appropriate type but has an
unexpected/invalid value.
term_image.exceptions.TermImageException: Auto font ratio is not supported
in the :term:`active terminal` or on the current platform.

This value is taken into consideration when setting image sizes for **text-based**
render styles, in order for images drawn to the terminal to have correct
proportion.

NOTE:
The font ratio is only required and used by text-based rendering styles, not
those based on terminal graphics protocols.
Changing the font ratio does not automatically affect any image that already
has it's size set. For a change in font ratio to have any effect, the image's
size has to be set again.

IMPORTANT:
Changing the font ratio does not automatically affect any image whose size has
already been set. For a change in font ratio to have any effect, it's size has
to be set again.
ATTENTION:
See :ref:`auto-font-ratio` for details about the auto modes.
"""
global _font_ratio

if not isinstance(ratio, float):
raise TypeError(f"Font ratio must be a float (got: {type(ratio).__name__})")
if ratio <= 0:
raise ValueError(f"Font ratio must be positive (got: {ratio})")

# cell-size == width * height
# font-ratio == width / height
_font_ratio = ratio


_font_ratio = 0.5 # Default
global _auto_font_ratio, _font_ratio

if isinstance(ratio, FontRatio):
if _auto_font_ratio is None:
_auto_font_ratio = get_cell_size() is not None

if not _auto_font_ratio:
raise TermImageException(
"Auto font ratio is not supported in the active terminal or on the "
"current platform"
)
elif ratio is FontRatio.AUTO:
# `(1, 2)` is a fallback in case the terminal doesn't respond in time
_font_ratio = truediv(*(get_cell_size() or (1, 2)))
else:
_font_ratio = None
elif isinstance(ratio, float):
if ratio <= 0.0:
raise ValueError(f"'ratio' must be greater than zero (got: {ratio})")
_font_ratio = ratio
else:
raise TypeError(
f"'ratio' must be a float or FontRatio enum (got: {type(ratio).__name__})"
)


class FontRatio(Enum):
"""Constants for auto font ratio modes"""

AUTO = auto()
FULL_AUTO = auto()


_font_ratio = 0.5
_auto_font_ratio = None
43 changes: 30 additions & 13 deletions term_image/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import PIL
import requests

from . import __version__, config, logging, notify, set_font_ratio, tui
from . import FontRatio, __version__, config, logging, notify, set_font_ratio, tui
from .config import config_options, store_config
from .exceptions import TermImageException, URLNotFoundError
from .exit_codes import FAILURE, INVALID_ARG, NO_VALID_SOURCE, SUCCESS
Expand Down Expand Up @@ -628,17 +628,6 @@ def check_arg(
action="store_true",
help="Restore default config and exit (Overwrites the config file)",
)
general.add_argument(
"-F",
"--font-ratio",
type=float,
metavar="N",
default=config.font_ratio,
help=(
"Specify the width-to-height ratio of a character cell in your terminal "
f"for proper image scaling (default: {config.font_ratio})"
),
)
general.add_argument(
"-S",
"--style",
Expand All @@ -658,6 +647,24 @@ def check_arg(
),
)

font_ratio_options = general.add_mutually_exclusive_group()
font_ratio_options.add_argument(
"-F",
"--font-ratio",
type=float,
metavar="N",
default=config.font_ratio,
help=(
"The width-to-height ratio of a character cell in the terminal, for "
f"correct image proportion (default: {config.font_ratio or 'auto'})"
),
)
font_ratio_options.add_argument(
"--auto-font-ratio",
action="store_true",
help="Determine the font ratio from the terminal, if possible",
)

mode_options = general.add_mutually_exclusive_group()
mode_options.add_argument(
"--cli",
Expand Down Expand Up @@ -1087,7 +1094,17 @@ def check_arg(
)
setattr(args, var_name, getattr(config, var_name))

set_font_ratio(args.font_ratio)
if args.auto_font_ratio:
args.font_ratio = None
try:
set_font_ratio(args.font_ratio or FontRatio.FULL_AUTO)
except TermImageException:
notify.notify(
"Auto font ratio is not supported in the active terminal or on this "
"platform, using 0.5. It can be set otherwise using `-F | --font-ratio`.",
level=notify.WARNING,
)
args.font_ratio = 0.5

ImageClass = {"auto": None, "kitty": KittyImage, "term": TermImage}[args.style]
if not ImageClass:
Expand Down
11 changes: 7 additions & 4 deletions term_image/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ def update_config(config: Dict[str, Any], old_version: str):
("['keys']['full-image']['Force Render'][1]", "F", "\u21e7F"),
("['keys']['full-grid-image']['Force Render'][1]", "F", "\u21e7F"),
],
"0.3": [
("['font ratio']", 0.5, None),
],
}

versions = tuple(changes)
Expand Down Expand Up @@ -318,7 +321,7 @@ def update_context_nav_keys(

user_dir = os.path.join(os.path.expanduser("~"), ".term_image")
config_file = os.path.join(user_dir, "config.json")
version = "0.2" # For config upgrades
version = "0.3" # For config upgrades

_valid_keys = {*bytes(range(32, 127)).decode(), *urwid.escape._keyconv.values(), "esc"}
_valid_keys.update(
Expand Down Expand Up @@ -368,7 +371,7 @@ def update_context_nav_keys(
_anim_cache = 100
_cell_width = 30
_checkers = None
_font_ratio = 0.5
_font_ratio = None
_getters = 4
_grid_renderers = 1
_log_file = os.path.join(user_dir, "term_image.log")
Expand Down Expand Up @@ -495,8 +498,8 @@ def update_context_nav_keys(
"must be `null` or a non-negative integer",
),
"font ratio": (
lambda x: isinstance(x, float) and x > 0.0,
"must be a float greater than zero",
lambda x: x is None or isinstance(x, float) and x > 0.0,
"must be `null` or a float greater than zero",
),
"getters": (
lambda x: isinstance(x, int) and x > 0,
Expand Down
Loading