mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-11-01 00:59:33 +01: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:
@@ -64,3 +64,6 @@ def common_error(message):
|
||||
"""Show a generic error dialog with the provided message."""
|
||||
dlg = wx.MessageDialog(None, message, _("Error"), wx.OK | wx.ICON_ERROR)
|
||||
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 builtins
|
||||
import requests
|
||||
from io import BytesIO
|
||||
from threading import Thread
|
||||
from pubsub import pub
|
||||
|
||||
from mysc.image_utils import load_scaled_image
|
||||
|
||||
_ = getattr(builtins, "_", lambda s: s)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -233,14 +234,14 @@ class ShowUserProfileDialog(wx.Dialog):
|
||||
"""Draws downloaded images on the bitmap controls."""
|
||||
try:
|
||||
if banner_bytes:
|
||||
banner_image = wx.Image(BytesIO(banner_bytes), wx.BITMAP_TYPE_ANY)
|
||||
banner_image.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
|
||||
self.bannerImage.SetBitmap(banner_image.ConvertToBitmap())
|
||||
banner_image = load_scaled_image(banner_bytes, 300, 100)
|
||||
if banner_image is not None:
|
||||
self.bannerImage.SetBitmap(banner_image.ConvertToBitmap())
|
||||
|
||||
if avatar_bytes:
|
||||
avatar_image = wx.Image(BytesIO(avatar_bytes), wx.BITMAP_TYPE_ANY)
|
||||
avatar_image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
||||
self.avatarImage.SetBitmap(avatar_image.ConvertToBitmap())
|
||||
avatar_image = load_scaled_image(avatar_bytes, 150, 150)
|
||||
if avatar_image is not None:
|
||||
self.avatarImage.SetBitmap(avatar_image.ConvertToBitmap())
|
||||
|
||||
self.Layout()
|
||||
self.Fit()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Wx dialogs for showing a user's profile."""
|
||||
|
||||
from io import BytesIO
|
||||
from pubsub import pub
|
||||
from typing import Tuple
|
||||
import requests
|
||||
@@ -10,6 +9,7 @@ from logging import getLogger
|
||||
from threading import Thread
|
||||
|
||||
from sessions.mastodon.utils import html_filter
|
||||
from mysc.image_utils import load_scaled_image
|
||||
|
||||
|
||||
log = getLogger(__name__)
|
||||
@@ -229,11 +229,11 @@ class ShowUserProfile(wx.Dialog):
|
||||
"""Draws images on the bitmap ui"""
|
||||
# log.debug("Drawing images...")
|
||||
# Header
|
||||
headerImage = wx.Image(BytesIO(headerImageBytes), wx.BITMAP_TYPE_ANY)
|
||||
headerImage.Rescale(300, 100, wx.IMAGE_QUALITY_HIGH)
|
||||
self.headerImage.SetBitmap(headerImage.ConvertToBitmap())
|
||||
headerImage = load_scaled_image(headerImageBytes, 300, 100)
|
||||
if headerImage is not None:
|
||||
self.headerImage.SetBitmap(headerImage.ConvertToBitmap())
|
||||
|
||||
# Avatar
|
||||
avatarImage = wx.Image(BytesIO(avatarImageBytes), wx.BITMAP_TYPE_ANY)
|
||||
avatarImage.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
||||
self.avatarImage.SetBitmap(avatarImage.ConvertToBitmap())
|
||||
avatarImage = load_scaled_image(avatarImageBytes, 150, 150)
|
||||
if avatarImage is not None:
|
||||
self.avatarImage.SetBitmap(avatarImage.ConvertToBitmap())
|
||||
|
||||
@@ -1,15 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import requests
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
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():
|
||||
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):
|
||||
"""
|
||||
A dialog for user to update his / her profile details.
|
||||
@@ -56,16 +84,12 @@ class UpdateProfileDialog(wx.Dialog):
|
||||
|
||||
# header
|
||||
header_label = wx.StaticText(panel, label=_("Header"))
|
||||
try:
|
||||
response = requests.get(self.header)
|
||||
except requests.exceptions.RequestException:
|
||||
# Create empty image
|
||||
self.header_image = wx.StaticBitmap()
|
||||
header_bitmap = download_scaled_image(self.header, 300, 100)
|
||||
if header_bitmap is None:
|
||||
# The image is unavailable or in a format we cannot decode, so show an empty placeholder.
|
||||
self.header_image = wx.StaticBitmap(panel)
|
||||
else:
|
||||
image_bytes = BytesIO(response.content)
|
||||
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 = wx.StaticBitmap(panel, bitmap=header_bitmap.ConvertToBitmap())
|
||||
|
||||
self.header_image.AcceptsFocusFromKeyboard = return_true
|
||||
self.change_header = wx.Button(panel, label=_("Change &header"))
|
||||
@@ -77,16 +101,12 @@ class UpdateProfileDialog(wx.Dialog):
|
||||
|
||||
# avatar
|
||||
avatar_label = wx.StaticText(panel, label=_("Avatar"))
|
||||
try:
|
||||
response = requests.get(self.avatar)
|
||||
except requests.exceptions.RequestException:
|
||||
# Create empty image
|
||||
self.avatar_image = wx.StaticBitmap()
|
||||
avatar_bitmap = download_scaled_image(self.avatar, 150, 150)
|
||||
if avatar_bitmap is None:
|
||||
# The image is unavailable or in a format we cannot decode, so show an empty placeholder.
|
||||
self.avatar_image = wx.StaticBitmap(panel)
|
||||
else:
|
||||
image_bytes = BytesIO(response.content)
|
||||
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 = wx.StaticBitmap(panel, bitmap=avatar_bitmap.ConvertToBitmap())
|
||||
|
||||
self.avatar_image.AcceptsFocusFromKeyboard = return_true
|
||||
self.change_avatar = wx.Button(panel, label=_("Change &avatar"))
|
||||
@@ -163,8 +183,7 @@ class UpdateProfileDialog(wx.Dialog):
|
||||
def on_change_header(self, *args):
|
||||
"""Display a dialog for the user to choose a picture and update the
|
||||
appropriate attribute"""
|
||||
wildcard = "Images (*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif"
|
||||
dlg = wx.FileDialog(self, _("Select header image - max 2MB"), wildcard=wildcard)
|
||||
dlg = wx.FileDialog(self, _("Select header image - max 2MB"), wildcard=IMAGE_WILDCARD)
|
||||
if dlg.ShowModal() == wx.CLOSE:
|
||||
return
|
||||
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
|
||||
|
||||
self.header = dlg.GetPath()
|
||||
image = wx.Image(self.header, wx.BITMAP_TYPE_ANY)
|
||||
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
||||
image = load_scaled_image(self.header, 150, 150)
|
||||
if image is None:
|
||||
return commonMessageDialogs.unsupported_image()
|
||||
self.header_image.SetBitmap(image.ConvertToBitmap())
|
||||
|
||||
def on_change_avatar(self, *args):
|
||||
"""Display a dialog for the user to choose a picture and update the
|
||||
appropriate attribute"""
|
||||
wildcard = "Images (*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif"
|
||||
dlg = wx.FileDialog(self, _("Select avatar image - max 2MB"), wildcard=wildcard)
|
||||
dlg = wx.FileDialog(self, _("Select avatar image - max 2MB"), wildcard=IMAGE_WILDCARD)
|
||||
if dlg.ShowModal() == wx.CLOSE:
|
||||
return
|
||||
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
|
||||
|
||||
self.avatar = dlg.GetPath()
|
||||
image = wx.Image(self.avatar, wx.BITMAP_TYPE_ANY)
|
||||
image.Rescale(150, 150, wx.IMAGE_QUALITY_HIGH)
|
||||
image = load_scaled_image(self.avatar, 150, 150)
|
||||
if image is None:
|
||||
return commonMessageDialogs.unsupported_image()
|
||||
self.avatar_image.SetBitmap(image.ConvertToBitmap())
|
||||
|
||||
Reference in New Issue
Block a user