mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-08-03 21:21:18 +02:00
Fix "Unknown image format" error when showing or updating profiles
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.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Tests for mysc.image_utils.
|
||||
|
||||
These are regression tests for the "Unknown image format" modal error dialog
|
||||
that wx raised when a Mastodon instance served profile pictures in a format
|
||||
wx cannot decode by itself, such as WebP.
|
||||
"""
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
import wx
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from mysc import image_utils
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def app():
|
||||
""" wx needs an application object before image objects can be created. """
|
||||
application = wx.App()
|
||||
yield application
|
||||
application.Destroy()
|
||||
|
||||
|
||||
def make_image_bytes(image_format, mode="RGB", size=(64, 32), color=(10, 20, 30)):
|
||||
""" Generates an in memory image in the requested format. """
|
||||
image = PILImage.new(mode, size, color)
|
||||
buffer = BytesIO()
|
||||
image.save(buffer, format=image_format)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("image_format", ["PNG", "JPEG", "GIF", "WEBP", "BMP"])
|
||||
def test_decode_image_supports_common_formats(image_format):
|
||||
""" All formats served by fediverse instances should decode, WebP included. """
|
||||
width, height, rgb_data, alpha_data = image_utils.decode_image(make_image_bytes(image_format))
|
||||
assert (width, height) == (64, 32)
|
||||
assert len(rgb_data) == 64 * 32 * 3
|
||||
assert alpha_data is None
|
||||
|
||||
|
||||
def test_decode_image_keeps_alpha_channel():
|
||||
""" Transparent images should keep their alpha channel separated for wx. """
|
||||
data = make_image_bytes("PNG", mode="RGBA", color=(10, 20, 30, 128))
|
||||
width, height, rgb_data, alpha_data = image_utils.decode_image(data)
|
||||
assert len(rgb_data) == width * height * 3
|
||||
assert alpha_data is not None
|
||||
assert len(alpha_data) == width * height
|
||||
assert set(alpha_data) == {128}
|
||||
|
||||
|
||||
def test_decode_image_accepts_a_path(tmp_path):
|
||||
""" Images picked by the user are passed around as filesystem paths. """
|
||||
path = tmp_path / "avatar.webp"
|
||||
path.write_bytes(make_image_bytes("WEBP"))
|
||||
width, height, rgb_data, alpha_data = image_utils.decode_image(str(path))
|
||||
assert (width, height) == (64, 32)
|
||||
assert len(rgb_data) == 64 * 32 * 3
|
||||
|
||||
|
||||
def test_decode_image_raises_on_invalid_data():
|
||||
with pytest.raises(Exception):
|
||||
image_utils.decode_image(b"this is definitely not an image")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("image_format", ["PNG", "JPEG", "GIF", "WEBP"])
|
||||
def test_load_image_returns_a_wx_image(app, image_format):
|
||||
image = image_utils.load_image(make_image_bytes(image_format))
|
||||
assert image is not None
|
||||
assert image.IsOk()
|
||||
assert (image.GetWidth(), image.GetHeight()) == (64, 32)
|
||||
|
||||
|
||||
def test_load_image_sets_alpha_on_transparent_images(app):
|
||||
image = image_utils.load_image(make_image_bytes("PNG", mode="RGBA", color=(10, 20, 30, 128)))
|
||||
assert image is not None
|
||||
assert image.HasAlpha()
|
||||
|
||||
|
||||
def test_load_image_returns_none_on_invalid_data(app):
|
||||
""" Broken or unsupported data must not raise, so no error dialog is shown. """
|
||||
assert image_utils.load_image(b"not an image at all") is None
|
||||
|
||||
|
||||
def test_load_image_returns_none_on_empty_data(app):
|
||||
""" Servers answering with an empty body should not break profile dialogs. """
|
||||
assert image_utils.load_image(b"") is None
|
||||
|
||||
|
||||
def test_load_scaled_image_rescales(app):
|
||||
image = image_utils.load_scaled_image(make_image_bytes("WEBP"), 150, 150)
|
||||
assert image is not None
|
||||
assert (image.GetWidth(), image.GetHeight()) == (150, 150)
|
||||
|
||||
|
||||
def test_load_scaled_image_returns_none_on_invalid_data(app):
|
||||
assert image_utils.load_scaled_image(b"nope", 150, 150) is None
|
||||
Reference in New Issue
Block a user