mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-08-18 10:58:11 +02:00
wx only decodes the image formats its own handlers support. Mastodon instances commonly serve avatars and headers as WebP, so wx.Image failed and raised a modal "Unknown image format" dialog, which blocked the user from updating their profile or from reading someone's profile. Images are now decoded with Pillow and their raw pixel data handed over to wx, through a new mysc.image_utils module shared by the Mastodon and Bluesky profile dialogs. Undecodable or unreachable images log the problem and leave an empty placeholder instead of raising. Also accept WebP and JPEG files in the header/avatar file pickers, warn the user with a common message dialog when the picked file cannot be read, and fix the parentless wx.StaticBitmap placeholders that were created when a profile image could not be downloaded.
78 lines
3.0 KiB
Python
78 lines
3.0 KiB
Python
# -*- 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
|