From 919dbbe1f1ce162b93f63ca728cf2040c4efdd37 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 5 Aug 2023 15:00:45 -0500 Subject: [PATCH 1/4] delegate Image.mode and Image.size to the values on Image.im when available When setting the values on Image also try to update the values on Image.im. There isn't currently a way to update values in the other direction. --- Tests/test_image.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 13 +++++------ src/PIL/IcoImagePlugin.py | 20 ++++++++--------- src/PIL/Image.py | 37 ++++++++++++++++++++++++++++--- src/PIL/ImageFile.py | 3 +++ src/_imaging.c | 45 +++++++++++++++++++++++++++++++++++--- 7 files changed, 97 insertions(+), 25 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 36f24379a60..bf828ba68be 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -662,7 +662,7 @@ def test__new(self): blank_pa.palette = None def _make_new(base_image, im, palette_result=None): - new_im = base_image._new(im) + new_im = base_image._new(im.im) assert new_im.mode == im.mode assert new_im.size == im.size assert new_im.info == base_image.info diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 943842f770d..31d4b53b3f4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -328,8 +328,8 @@ def _seek(self, frame, update_image=True): self._mode = "RGBA" del self.info["transparency"] else: - self._mode = "RGB" self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) + self._mode = "RGB" def _rgb(color): if self._frame_palette: diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 0aa4f7a8458..5df3449b1c2 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -261,11 +261,7 @@ def _open(self): self.best_size[1] * self.best_size[2], ) - @property - def size(self): - return self._size - - @size.setter + @Image.Image.size.setter def size(self, value): info_size = value if info_size not in self.info["sizes"] and len(info_size) == 2: @@ -283,7 +279,10 @@ def size(self, value): if info_size not in self.info["sizes"]: msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) - self._size = value + if value != self.size: + self.im = None + self.pyaccess = None + self._size = value def load(self): if len(self.size) == 3: @@ -306,7 +305,7 @@ def load(self): self.im = im.im self._mode = im.mode - self.size = im.size + self._size = im.size return px diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 0445a2ab22f..3d75afb627a 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -310,36 +310,36 @@ def _open(self): self.size = self.ico.entry[0]["dim"] self.load() - @property - def size(self): - return self._size - - @size.setter + @Image.Image.size.setter def size(self, value): if value not in self.info["sizes"]: msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) - self._size = value + if value != self.size: + self.im = None + self.pyaccess = None + self._size = value def load(self): if self.im is not None and self.im.size == self.size: # Already loaded return Image.Image.load(self) - im = self.ico.getimage(self.size) + size_to_load = self.size + im = self.ico.getimage(size_to_load) # if tile is PNG, it won't really be loaded yet im.load() self.im = im.im self.pyaccess = None self._mode = im.mode - if im.size != self.size: + if im.size != size_to_load: warnings.warn("Image was not the expected size") - index = self.ico.getentryindex(self.size) + index = self.ico.getentryindex(size_to_load) sizes = list(self.info["sizes"]) sizes[index] = im.size self.info["sizes"] = set(sizes) - self.size = im.size + self._size = im.size def load_seek(self): # Flag the ImageFile.Parser so that it diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 476ed012278..172fe7fd44d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -480,16 +480,21 @@ class Image: def __init__(self): # FIXME: take "new" parameters / other image? - # FIXME: turn mode and size into delegating properties? self.im = None - self._mode = "" - self._size = (0, 0) + # do not directly change __mode; use _mode instead + self.__mode = "" + # do not directly change __size; use _size instead + self.__size = (0, 0) self.palette = None self.info = {} self.readonly = 0 self.pyaccess = None self._exif = None + def _use_im_values(self): + ''' Whether or not to try using values from self.im in addition to the values in this class. ''' + return self.im is not None + @property def width(self): return self.size[0] @@ -502,10 +507,36 @@ def height(self): def size(self): return self._size + @property + def _size(self): + if self._use_im_values(): + return self.im.size + return self.__size + + @_size.setter + def _size(self, value): + # set im.size first in case it raises an excepton + if self._use_im_values(): + self.im.size = value + self.__size = value + @property def mode(self): return self._mode + @property + def _mode(self): + if self._use_im_values(): + return self.im.mode + return self.__mode + + @_mode.setter + def _mode(self, value): + # set im.mode first in case it raises an excepton + if self._use_im_values(): + self.im.mode = value + self.__mode = value + def _new(self, im): new = Image() new.im = im diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 8e4f7dfb2c8..39187624971 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -139,6 +139,9 @@ def get_format_mimetype(self): if self.format is not None: return Image.MIME.get(self.format.upper()) + def _use_im_values(self): + return self.tile is None and self.im is not None + def __setstate__(self, state): self.tile = [] super().__setstate__(state) diff --git a/src/_imaging.c b/src/_imaging.c index e15cb89fcea..5c911d81a2e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3646,11 +3646,49 @@ _getattr_mode(ImagingObject *self, void *closure) { return PyUnicode_FromString(self->image->mode); } +static int +_setattr_mode(ImagingObject *self, PyObject *value, void *closure) { + if (value == NULL) { + self->image->mode[0] = '\0'; + return 0; + } + + const char *mode = PyUnicode_AsUTF8(value); + if (mode == NULL) { + return -1; + } + if (strlen(mode) >= IMAGING_MODE_LENGTH) { + PyErr_SetString(PyExc_ValueError, "given mode name is too long"); + return -1; + } + + strcpy(self->image->mode, mode); + return 0; +} + static PyObject * _getattr_size(ImagingObject *self, void *closure) { return Py_BuildValue("ii", self->image->xsize, self->image->ysize); } +static int +_setattr_size(ImagingObject *self, PyObject *value, void *closure) { + if (value == NULL) { + self->image->xsize = 0; + self->image->ysize = 0; + return 0; + } + + int xsize, ysize; + if (!PyArg_ParseTuple(value, "ii", &xsize, &ysize)) { + return -1; + } + + self->image->xsize = xsize; + self->image->ysize = ysize; + return 0; +} + static PyObject * _getattr_bands(ImagingObject *self, void *closure) { return PyLong_FromLong(self->image->bands); @@ -3679,13 +3717,14 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) { }; static struct PyGetSetDef getsetters[] = { - {"mode", (getter)_getattr_mode}, - {"size", (getter)_getattr_size}, + {"mode", (getter)_getattr_mode, (setter)_setattr_mode}, + {"size", (getter)_getattr_size, (setter)_setattr_size}, {"bands", (getter)_getattr_bands}, {"id", (getter)_getattr_id}, {"ptr", (getter)_getattr_ptr}, {"unsafe_ptrs", (getter)_getattr_unsafe_ptrs}, - {NULL}}; + {NULL} +}; /* basic sequence semantics */ From b879ada2a2d732d3d09876fc73aa705c8b5850ff Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 5 Aug 2023 15:10:57 -0500 Subject: [PATCH 2/4] split long comment across two lines and use double quotes instead of single quotes --- src/PIL/Image.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 172fe7fd44d..7e545da8c6d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -492,7 +492,10 @@ def __init__(self): self._exif = None def _use_im_values(self): - ''' Whether or not to try using values from self.im in addition to the values in this class. ''' + """ + Whether or not to try using values from self.im + in addition to the values in this class. + """ return self.im is not None @property From 0a88b5b33849d451c96c2bd66d3040809ccfeb4f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 7 Aug 2023 10:35:52 -0500 Subject: [PATCH 3/4] fix typo in comment --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7e545da8c6d..7b45062fdde 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -518,7 +518,7 @@ def _size(self): @_size.setter def _size(self, value): - # set im.size first in case it raises an excepton + # set im.size first in case it raises an exception if self._use_im_values(): self.im.size = value self.__size = value @@ -535,7 +535,7 @@ def _mode(self): @_mode.setter def _mode(self, value): - # set im.mode first in case it raises an excepton + # set im.mode first in case it raises an exception if self._use_im_values(): self.im.mode = value self.__mode = value From 555420b8a335f1bbe5be44f57b902f121c063b7f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 7 Aug 2023 10:58:52 -0500 Subject: [PATCH 4/4] remove _size and _mode getters --- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 2 +- src/PIL/Image.py | 14 ++++---------- src/PIL/ImageOps.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/QoiImagePlugin.py | 2 +- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index b96ce9603a3..195dde86367 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -360,7 +360,7 @@ def check_required_header_comments(): check_required_header_comments() - if not self._size: + if not self.size: msg = "cannot determine EPS bounding box" raise OSError(msg) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 31d4b53b3f4..b3a7077b6d4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -262,7 +262,7 @@ def _seek(self, frame, update_image=True): x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6) if (x1 > self.size[0] or y1 > self.size[1]) and update_image: self._size = max(x1, self.size[0]), max(y1, self.size[1]) - Image._decompression_bomb_check(self._size) + Image._decompression_bomb_check(self.size) frame_dispose_extent = x0, y0, x1, y1 flags = s[8] diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7b45062fdde..f09429875df 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -508,38 +508,32 @@ def height(self): @property def size(self): - return self._size - - @property - def _size(self): if self._use_im_values(): return self.im.size return self.__size - @_size.setter def _size(self, value): # set im.size first in case it raises an exception if self._use_im_values(): self.im.size = value self.__size = value - @property - def mode(self): - return self._mode + _size = property(fset=_size) @property - def _mode(self): + def mode(self): if self._use_im_values(): return self.im.mode return self.__mode - @_mode.setter def _mode(self, value): # set im.mode first in case it raises an exception if self._use_im_values(): self.im.mode = value self.__mode = value + _mode = property(fset=_mode) + def _new(self, im): new = Image() new.im = im diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 17702778c13..92561860282 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -604,7 +604,7 @@ def exif_transpose(image, *, in_place=False): if in_place: image.im = transposed_image.im image.pyaccess = None - image._size = transposed_image._size + image._size = transposed_image.size exif_image = image if in_place else transposed_image exif = exif_image.getexif() diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 854d9e83ee7..f4a636098a4 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -114,7 +114,7 @@ def _open(self): # Don't trust the passed in stride. # Calculate the approximate position for ourselves. # CVE-2020-35653 - stride = (self._size[0] * bits + 7) // 8 + stride = (self.size[0] * bits + 7) // 8 # While the specification states that this must be even, # not all images follow this diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 5c34075038f..e51e739af2e 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -32,7 +32,7 @@ def _open(self): self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] + self.tile = [("qoi", (0, 0) + self.size, self.fp.tell(), None)] class QoiDecoder(ImageFile.PyDecoder):