Files
twblue/src/mysc/image_utils.py
T

78 lines
3.0 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
""" Helpers to turn arbitrary image data into wx.Image objects.
wx only knows about the image formats its own handlers support. Fediverse
instances (and Bluesky) happily serve avatars and headers in formats such as
WebP, which makes wx.Image fail and pop up a modal "Unknown image format"
error dialog, blocking the user. Pillow understands many more formats, so we
decode everything with it and hand the raw pixel data over to wx.
"""
from io import BytesIO
from logging import getLogger
from typing import Optional, Tuple, Union
import wx
from PIL import Image as PILImage
log = getLogger("mysc.image_utils")
ImageSource = Union[bytes, bytearray, str]
#: Pillow modes that carry per pixel transparency.
_TRANSPARENT_MODES = ("RGBA", "LA", "PA")
def decode_image(source: ImageSource) -> Tuple[int, int, bytes, Optional[bytes]]:
""" Decodes an image into raw pixel data suitable for wx.
:param source: Raw image bytes or a path to an image file.
:returns: A (width, height, rgb_data, alpha_data) tuple. alpha_data is None
when the image has no transparency.
:raises Exception: Whatever Pillow raises when the data cannot be decoded.
"""
if isinstance(source, (bytes, bytearray)):
source = BytesIO(bytes(source))
with PILImage.open(source) as image:
image.load()
has_alpha = image.mode in _TRANSPARENT_MODES or (image.mode == "P" and "transparency" in image.info)
if has_alpha:
image = image.convert("RGBA")
return image.width, image.height, image.convert("RGB").tobytes(), image.getchannel("A").tobytes()
image = image.convert("RGB")
return image.width, image.height, image.tobytes(), None
def load_image(source: ImageSource) -> Optional[wx.Image]:
""" Builds a wx.Image from raw image bytes or a path to an image file.
:param source: Raw image bytes or a path to an image file.
:returns: The decoded wx.Image, or None if the data could not be decoded.
Callers are expected to skip drawing when None is returned, instead of
letting wx show its own error dialog.
"""
try:
width, height, rgb_data, alpha_data = decode_image(source)
except Exception:
log.exception("Unable to decode image data.")
return None
image = wx.Image(width, height)
image.SetData(rgb_data)
if alpha_data is not None:
image.SetAlpha(alpha_data)
return image
def load_scaled_image(source: ImageSource, width: int, height: int) -> Optional[wx.Image]:
""" Same as load_image, but rescales the result to the given size.
:param source: Raw image bytes or a path to an image file.
:param width: Width, in pixels, of the resulting image.
:param height: Height, in pixels, of the resulting image.
:returns: The rescaled wx.Image, or None if the data could not be decoded.
"""
image = load_image(source)
if image is None:
return None
image.Rescale(width, height, wx.IMAGE_QUALITY_HIGH)
return image