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:
Juanjo M
2026-08-02 14:32:45 +01:00
parent 130afe3432
commit b6681df03c
8 changed files with 241 additions and 42 deletions
+97
View File
@@ -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