Convert UnidentifiedImageError from PIL into validation error

The image conversion that are done as preprocessing of the user input should be
considered as data validation.

Closes #14084
This commit is contained in:
Cédric Krier
2025-06-18 01:26:39 +02:00
parent 2cd40e46a8
commit 6718a613cf
9 changed files with 56 additions and 4 deletions
+8 -1
View File
@@ -12,6 +12,7 @@ except ImportError:
PIL = None
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
MatchMixin, ModelSQL, ModelView, Unique, fields, sequence_ordered)
from trytond.model.exceptions import SQLConstraintError
@@ -22,6 +23,8 @@ from trytond.tools import timezone as tz
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
from .exceptions import LogoValidationError
SIZE_MAX = config.getint('company', 'logo_size_max', default=2048)
TIMEZONES = [(z, z) for z in tz.available_timezones()]
TIMEZONES += [(None, '')]
@@ -118,9 +121,13 @@ class Company(ModelSQL, ModelView):
@classmethod
def _logo_convert(cls, image, **_params):
if not PIL:
return
return image
data = io.BytesIO()
try:
img = PIL.Image.open(io.BytesIO(image))
except PIL.UnidentifiedImageError as e:
raise LogoValidationError(gettext(
'company.msg_company_logo_error'), str(e)) from e
img.thumbnail((SIZE_MAX, SIZE_MAX))
if img.mode != 'RGBA':
img = img.convert('RGBA')
+8
View File
@@ -0,0 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model.exceptions import ValidationError
class LogoValidationError(ValidationError):
pass
+3
View File
@@ -6,5 +6,8 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_company_logo_cache_size_unique">
<field name="text">The size of cached company logo must be unique.</field>
</record>
<record model="ir.message" id="msg_company_logo_error">
<field name="text">Can not open or identify the image set for the company logo.</field>
</record>
</data>
</tryton>
+8
View File
@@ -0,0 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model.exceptions import ValidationError
class ImageValidationError(ValidationError):
pass
+3
View File
@@ -6,5 +6,8 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_image_cache_size_unique">
<field name="text">The size of cached image must be unique.</field>
</record>
<record model="ir.message" id="msg_product_image_error">
<field name="text">Can not open or identify the image set for the product.</field>
</record>
</data>
</tryton>
+8
View File
@@ -4,9 +4,11 @@ import io
import math
from urllib.parse import quote, urlencode, urljoin
import PIL
import PIL.Image
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
MatchMixin, ModelSQL, ModelView, Unique, fields, sequence_ordered)
from trytond.pool import PoolMeta
@@ -16,6 +18,8 @@ from trytond.transaction import Transaction
from trytond.url import http_host
from trytond.wsgi import Base64Converter
from .exceptions import ImageValidationError
if config.getboolean('product', 'image_filestore', default=False):
file_id = 'image_id'
store_prefix = config.get('product', 'image_prefix', default=None)
@@ -162,7 +166,11 @@ class ImageMixin(_ImageMixin):
@classmethod
def convert(cls, image, **_params):
data = io.BytesIO()
try:
img = PIL.Image.open(io.BytesIO(image))
except PIL.UnidentifiedImageError as e:
raise ImageValidationError(gettext(
'product_image.msg_product_image_error'), str(e)) from e
img.thumbnail((SIZE_MAX, SIZE_MAX))
if img.mode != 'RGB':
img = img.convert('RGBA')
+10
View File
@@ -14,7 +14,9 @@ except ImportError:
PIL = None
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import ModelSQL, Unique, fields
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.wsgi import Base64Converter
@@ -31,6 +33,10 @@ URL_BASE = config.get('web', 'avatar_base', default='')
FONT = os.path.join(os.path.dirname(__file__), 'fonts', 'karla.ttf')
class AvatarValidationError(ValidationError):
pass
class ImageMixin:
__slots__ = ()
image = fields.Binary(
@@ -118,7 +124,11 @@ class Avatar(ImageMixin, ResourceMixin, ModelSQL):
if not PIL or not image:
return image
data = io.BytesIO()
try:
img = Image.open(io.BytesIO(image))
except PIL.UnidentifiedImageError as e:
raise AvatarValidationError(gettext(
'ir.msg_avatar_image_error'), str(e)) from e
width, height = img.size
size = min(width, height)
img = img.crop((
+2
View File
@@ -1,6 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from .avatar import AvatarValidationError
from .chat import InvalidEMailError as ChatInvalidEmailError
from .lang import DateError as LanguageDateError
from .lang import DeleteDefaultError as LanguageDeleteDefaultError
@@ -14,6 +15,7 @@ from .translation import OverriddenError as TranslationOverriddenError
from .trigger import ConditionError as TriggerConditionError
__all__ = [
AvatarValidationError,
ChatInvalidEmailError,
DeactivateDependencyError,
LanguageDateError,
+3
View File
@@ -418,6 +418,9 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_avatar_size_unique">
<field name="text">The size of an avatar must be unique.</field>
</record>
<record model="ir.message" id="msg_avatar_image_error">
<field name="text">Can not open or identify the image set as avatar.</field>
</record>
<record model="ir.message" id="msg_avatar">
<field name="text">Avatar</field>
</record>