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:
@@ -53,6 +53,7 @@ This version introduces comprehensive support for the AT Protocol (ATProto), ena
|
|||||||
* Mastodon:
|
* Mastodon:
|
||||||
* Added support for sending quoted posts! You can now quote other users' posts from the context menu or the new Boost dialog. ([#860](https://github.com/mcv-software/twblue/issues/860))
|
* Added support for sending quoted posts! You can now quote other users' posts from the context menu or the new Boost dialog. ([#860](https://github.com/mcv-software/twblue/issues/860))
|
||||||
* Fixed an issue where HTML entities were not decoded when editing a post. ([#893](https://github.com/mcv-software/twblue/issues/893))
|
* Fixed an issue where HTML entities were not decoded when editing a post. ([#893](https://github.com/mcv-software/twblue/issues/893))
|
||||||
|
* Fixed an error that displayed an "Unknown image format" dialog when viewing someone's profile or updating your own, if the avatar or header was served in a format TWBlue could not read, such as WebP. ([#977](https://github.com/mcv-software/twblue/issues/977))
|
||||||
|
|
||||||
## Changes in version 2026.01.13
|
## Changes in version 2026.01.13
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# -*- 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
|
||||||
@@ -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
|
||||||
@@ -64,3 +64,6 @@ def common_error(message):
|
|||||||
"""Show a generic error dialog with the provided message."""
|
"""Show a generic error dialog with the provided message."""
|
||||||
dlg = wx.MessageDialog(None, message, _("Error"), wx.OK | wx.ICON_ERROR)
|
dlg = wx.MessageDialog(None, message, _("Error"), wx.OK | wx.ICON_ERROR)
|
||||||
return dlg.ShowModal()
|
return dlg.ShowModal()
|
||||||
|
|
||||||
|
def unsupported_image():
|
||||||
|
return wx.MessageDialog(None, _("TWBlue was unable to load the selected image. Please make sure the file is a valid image and try again."), _("Error"), wx.ICON_ERROR).ShowModal()
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import logging
|
|||||||
import languageHandler
|
import languageHandler
|
||||||
import builtins
|
import builtins
|
||||||
import requests
|
import requests
|
||||||
from io import BytesIO
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from pubsub import pub
|
from pubsub import pub
|
||||||
|
|
||||||
|
from mysc.image_utils import load_scaled_image
|
||||||
|
|
||||||
_ = getattr(builtins, "_", lambda s: s)
|
_ = getattr(builtins, "_", lambda s: s)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -233,13 +234,13 @@ class ShowUserProfileDialog(wx.Dialog):
|
|||||||
"""Draws downloaded images on the bitmap controls."""
|
"""Draws downloaded images on the bitmap controls."""
|
||||||
try:
|
try:
|
||||||
if banner_bytes:
|
if banner_bytes:
|
||||||
banner_image = wx.Image(BytesIO(banner_bytes), wx.BITMAP_TYPE_ANY)
|
banner_image = load_scaled_image(banner_bytes, 300, 100)
|
||||||
banner_image.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
|
if banner_image is not None:
|
||||||
self.bannerImage.SetBitmap(banner_image.ConvertToBitmap())
|
self.bannerImage.SetBitmap(banner_image.ConvertToBitmap())
|
||||||
|
|
||||||
if avatar_bytes:
|
if avatar_bytes:
|
||||||
avatar_image = wx.Image(BytesIO(avatar_bytes), wx.BITMAP_TYPE_ANY)
|
avatar_image = load_scaled_image(avatar_bytes, 150, 150)
|
||||||
avatar_image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
if avatar_image is not None:
|
||||||
self.avatarImage.SetBitmap(avatar_image.ConvertToBitmap())
|
self.avatarImage.SetBitmap(avatar_image.ConvertToBitmap())
|
||||||
|
|
||||||
self.Layout()
|
self.Layout()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Wx dialogs for showing a user's profile."""
|
"""Wx dialogs for showing a user's profile."""
|
||||||
|
|
||||||
from io import BytesIO
|
|
||||||
from pubsub import pub
|
from pubsub import pub
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import requests
|
import requests
|
||||||
@@ -10,6 +9,7 @@ from logging import getLogger
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from sessions.mastodon.utils import html_filter
|
from sessions.mastodon.utils import html_filter
|
||||||
|
from mysc.image_utils import load_scaled_image
|
||||||
|
|
||||||
|
|
||||||
log = getLogger(__name__)
|
log = getLogger(__name__)
|
||||||
@@ -229,11 +229,11 @@ class ShowUserProfile(wx.Dialog):
|
|||||||
"""Draws images on the bitmap ui"""
|
"""Draws images on the bitmap ui"""
|
||||||
# log.debug("Drawing images...")
|
# log.debug("Drawing images...")
|
||||||
# Header
|
# Header
|
||||||
headerImage = wx.Image(BytesIO(headerImageBytes), wx.BITMAP_TYPE_ANY)
|
headerImage = load_scaled_image(headerImageBytes, 300, 100)
|
||||||
headerImage.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
|
if headerImage is not None:
|
||||||
self.headerImage.SetBitmap(headerImage.ConvertToBitmap())
|
self.headerImage.SetBitmap(headerImage.ConvertToBitmap())
|
||||||
|
|
||||||
# Avatar
|
# Avatar
|
||||||
avatarImage = wx.Image(BytesIO(avatarImageBytes), wx.BITMAP_TYPE_ANY)
|
avatarImage = load_scaled_image(avatarImageBytes, 150, 150)
|
||||||
avatarImage.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
if avatarImage is not None:
|
||||||
self.avatarImage.SetBitmap(avatarImage.ConvertToBitmap())
|
self.avatarImage.SetBitmap(avatarImage.ConvertToBitmap())
|
||||||
|
|||||||
@@ -1,15 +1,43 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
import requests
|
from logging import getLogger
|
||||||
from io import BytesIO
|
from typing import Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
import wx
|
import wx
|
||||||
|
|
||||||
|
from mysc.image_utils import load_scaled_image
|
||||||
|
from wxUI import commonMessageDialogs
|
||||||
|
|
||||||
|
log = getLogger("wxUI.dialogs.mastodon.updateProfile")
|
||||||
|
|
||||||
|
#: Formats accepted when the user picks a new header or avatar.
|
||||||
|
IMAGE_WILDCARD = "Images (*.png;*.jpg;*.jpeg;*.gif;*.webp)|*.png;*.jpg;*.jpeg;*.gif;*.webp"
|
||||||
|
|
||||||
|
|
||||||
def return_true():
|
def return_true():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def download_scaled_image(url: str, width: int, height: int) -> Optional[wx.Image]:
|
||||||
|
""" Downloads an image and decodes it, scaled to the given size.
|
||||||
|
|
||||||
|
:param url: URL of the image to download.
|
||||||
|
:param width: Width, in pixels, of the resulting image.
|
||||||
|
:param height: Height, in pixels, of the resulting image.
|
||||||
|
:returns: The decoded wx.Image, or None if it could not be downloaded or decoded.
|
||||||
|
"""
|
||||||
|
if not url:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
log.exception("Unable to download image from %s", url)
|
||||||
|
return None
|
||||||
|
return load_scaled_image(response.content, width, height)
|
||||||
|
|
||||||
|
|
||||||
class UpdateProfileDialog(wx.Dialog):
|
class UpdateProfileDialog(wx.Dialog):
|
||||||
"""
|
"""
|
||||||
A dialog for user to update his / her profile details.
|
A dialog for user to update his / her profile details.
|
||||||
@@ -56,16 +84,12 @@ class UpdateProfileDialog(wx.Dialog):
|
|||||||
|
|
||||||
# header
|
# header
|
||||||
header_label = wx.StaticText(panel, label=_("Header"))
|
header_label = wx.StaticText(panel, label=_("Header"))
|
||||||
try:
|
header_bitmap = download_scaled_image(self.header, 300, 100)
|
||||||
response = requests.get(self.header)
|
if header_bitmap is None:
|
||||||
except requests.exceptions.RequestException:
|
# The image is unavailable or in a format we cannot decode, so show an empty placeholder.
|
||||||
# Create empty image
|
self.header_image = wx.StaticBitmap(panel)
|
||||||
self.header_image = wx.StaticBitmap()
|
|
||||||
else:
|
else:
|
||||||
image_bytes = BytesIO(response.content)
|
self.header_image = wx.StaticBitmap(panel, bitmap=header_bitmap.ConvertToBitmap())
|
||||||
image = wx.Image(image_bytes, wx.BITMAP_TYPE_ANY)
|
|
||||||
image.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
|
|
||||||
self.header_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap())
|
|
||||||
|
|
||||||
self.header_image.AcceptsFocusFromKeyboard = return_true
|
self.header_image.AcceptsFocusFromKeyboard = return_true
|
||||||
self.change_header = wx.Button(panel, label=_("Change &header"))
|
self.change_header = wx.Button(panel, label=_("Change &header"))
|
||||||
@@ -77,16 +101,12 @@ class UpdateProfileDialog(wx.Dialog):
|
|||||||
|
|
||||||
# avatar
|
# avatar
|
||||||
avatar_label = wx.StaticText(panel, label=_("Avatar"))
|
avatar_label = wx.StaticText(panel, label=_("Avatar"))
|
||||||
try:
|
avatar_bitmap = download_scaled_image(self.avatar, 150, 150)
|
||||||
response = requests.get(self.avatar)
|
if avatar_bitmap is None:
|
||||||
except requests.exceptions.RequestException:
|
# The image is unavailable or in a format we cannot decode, so show an empty placeholder.
|
||||||
# Create empty image
|
self.avatar_image = wx.StaticBitmap(panel)
|
||||||
self.avatar_image = wx.StaticBitmap()
|
|
||||||
else:
|
else:
|
||||||
image_bytes = BytesIO(response.content)
|
self.avatar_image = wx.StaticBitmap(panel, bitmap=avatar_bitmap.ConvertToBitmap())
|
||||||
image = wx.Image(image_bytes, wx.BITMAP_TYPE_ANY)
|
|
||||||
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
|
||||||
self.avatar_image = wx.StaticBitmap(panel, bitmap=image.ConvertToBitmap())
|
|
||||||
|
|
||||||
self.avatar_image.AcceptsFocusFromKeyboard = return_true
|
self.avatar_image.AcceptsFocusFromKeyboard = return_true
|
||||||
self.change_avatar = wx.Button(panel, label=_("Change &avatar"))
|
self.change_avatar = wx.Button(panel, label=_("Change &avatar"))
|
||||||
@@ -163,8 +183,7 @@ class UpdateProfileDialog(wx.Dialog):
|
|||||||
def on_change_header(self, *args):
|
def on_change_header(self, *args):
|
||||||
"""Display a dialog for the user to choose a picture and update the
|
"""Display a dialog for the user to choose a picture and update the
|
||||||
appropriate attribute"""
|
appropriate attribute"""
|
||||||
wildcard = "Images (*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif"
|
dlg = wx.FileDialog(self, _("Select header image - max 2MB"), wildcard=IMAGE_WILDCARD)
|
||||||
dlg = wx.FileDialog(self, _("Select header image - max 2MB"), wildcard=wildcard)
|
|
||||||
if dlg.ShowModal() == wx.CLOSE:
|
if dlg.ShowModal() == wx.CLOSE:
|
||||||
return
|
return
|
||||||
if os.path.getsize(dlg.GetPath()) > 2097152:
|
if os.path.getsize(dlg.GetPath()) > 2097152:
|
||||||
@@ -178,15 +197,15 @@ class UpdateProfileDialog(wx.Dialog):
|
|||||||
return self.on_change_header() if result == wx.YES else None
|
return self.on_change_header() if result == wx.YES else None
|
||||||
|
|
||||||
self.header = dlg.GetPath()
|
self.header = dlg.GetPath()
|
||||||
image = wx.Image(self.header, wx.BITMAP_TYPE_ANY)
|
image = load_scaled_image(self.header, 150, 150)
|
||||||
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
if image is None:
|
||||||
|
return commonMessageDialogs.unsupported_image()
|
||||||
self.header_image.SetBitmap(image.ConvertToBitmap())
|
self.header_image.SetBitmap(image.ConvertToBitmap())
|
||||||
|
|
||||||
def on_change_avatar(self, *args):
|
def on_change_avatar(self, *args):
|
||||||
"""Display a dialog for the user to choose a picture and update the
|
"""Display a dialog for the user to choose a picture and update the
|
||||||
appropriate attribute"""
|
appropriate attribute"""
|
||||||
wildcard = "Images (*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif"
|
dlg = wx.FileDialog(self, _("Select avatar image - max 2MB"), wildcard=IMAGE_WILDCARD)
|
||||||
dlg = wx.FileDialog(self, _("Select avatar image - max 2MB"), wildcard=wildcard)
|
|
||||||
if dlg.ShowModal() == wx.CLOSE:
|
if dlg.ShowModal() == wx.CLOSE:
|
||||||
return
|
return
|
||||||
if os.path.getsize(dlg.GetPath()) > 2097152:
|
if os.path.getsize(dlg.GetPath()) > 2097152:
|
||||||
@@ -200,6 +219,7 @@ class UpdateProfileDialog(wx.Dialog):
|
|||||||
return self.on_change_avatar() if result == wx.YES else None
|
return self.on_change_avatar() if result == wx.YES else None
|
||||||
|
|
||||||
self.avatar = dlg.GetPath()
|
self.avatar = dlg.GetPath()
|
||||||
image = wx.Image(self.avatar, wx.BITMAP_TYPE_ANY)
|
image = load_scaled_image(self.avatar, 150, 150)
|
||||||
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
if image is None:
|
||||||
|
return commonMessageDialogs.unsupported_image()
|
||||||
self.avatar_image.SetBitmap(image.ConvertToBitmap())
|
self.avatar_image.SetBitmap(image.ConvertToBitmap())
|
||||||
|
|||||||
Reference in New Issue
Block a user