diff options
Diffstat (limited to 'poezio/windows/image.py')
-rw-r--r-- | poezio/windows/image.py | 56 |
1 files changed, 52 insertions, 4 deletions
diff --git a/poezio/windows/image.py b/poezio/windows/image.py index 75f4d588..2862d2d9 100644 --- a/poezio/windows/image.py +++ b/poezio/windows/image.py @@ -2,6 +2,8 @@ Defines a window which contains either an image or a border. """ +from __future__ import annotations + import curses from io import BytesIO @@ -11,6 +13,15 @@ try: except ImportError: HAS_PIL = False +try: + import gi + gi.require_version('Rsvg', '2.0') + from gi.repository import Rsvg + import cairo + HAS_RSVG = True +except (ImportError, ValueError, AttributeError): + HAS_RSVG = False + from poezio.windows.base_wins import Win from poezio.theming import get_theme, to_curses_attr from poezio.xhtml import _parse_css_color @@ -19,6 +30,36 @@ from poezio.config import config from typing import Tuple, Optional, Callable +MAX_SIZE = 16 + + +def render_svg(svg: bytes) -> Optional[Image.Image]: + if not HAS_RSVG: + return None + try: + handle = Rsvg.Handle.new_from_data(svg) + dimensions = handle.get_dimensions() + biggest_dimension = max(dimensions.width, dimensions.height) + scale = MAX_SIZE / biggest_dimension + translate_x = (biggest_dimension - dimensions.width) / 2 + translate_y = (biggest_dimension - dimensions.height) / 2 + + surface = cairo.ImageSurface(cairo.Format.ARGB32, MAX_SIZE, MAX_SIZE) + context = cairo.Context(surface) + context.scale(scale, scale) + context.translate(translate_x, translate_y) + handle.render_cairo(context) + data = surface.get_data() + image = Image.frombytes('RGBA', (MAX_SIZE, MAX_SIZE), data.tobytes()) + # This is required because Cairo uses a BGRA (in host endianness) + # format, and PIL an ABGR (in byte order) format. Yes, this is + # confusing. + b, g, r, a = image.split() + return Image.merge('RGB', (r, g, b)) + except Exception: + return None + + class ImageWin(Win): """ A window which contains either an image or a border. @@ -27,10 +68,10 @@ class ImageWin(Win): __slots__ = ('_image', '_display_avatar') def __init__(self) -> None: - self._image = None # type: Optional[Image] + self._image: Optional[Image.Image] = None Win.__init__(self) - if config.get('image_use_half_blocks'): - self._display_avatar = self._display_avatar_half_blocks # type: Callable[[int, int], None] + if config.getbool('image_use_half_blocks'): + self._display_avatar: Callable[[int, int], None] = self._display_avatar_half_blocks else: self._display_avatar = self._display_avatar_full_blocks @@ -45,7 +86,14 @@ class ImageWin(Win): if data is not None and HAS_PIL: image_file = BytesIO(data) try: - image = Image.open(image_file) + try: + image = Image.open(image_file) + except OSError: + # TODO: Make the caller pass the MIME type, so we don’t + # have to try all renderers like that. + image = render_svg(data) + if image is None: + raise except OSError: self._display_border() else: |