From 919dbbe1f1ce162b93f63ca728cf2040c4efdd37 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 5 Aug 2023 15:00:45 -0500 Subject: [PATCH] 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 36f24379a..bf828ba68 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -662,7 +662,7 @@ class TestImage: 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 943842f77..31d4b53b3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -328,8 +328,8 @@ class GifImageFile(ImageFile.ImageFile): 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 0aa4f7a84..5df3449b1 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -261,11 +261,7 @@ class IcnsImageFile(ImageFile.ImageFile): 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 @@ class IcnsImageFile(ImageFile.ImageFile): 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 @@ class IcnsImageFile(ImageFile.ImageFile): 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 0445a2ab2..3d75afb62 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -310,36 +310,36 @@ class IcoImageFile(ImageFile.ImageFile): 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 476ed0122..172fe7fd4 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 @@ class Image: 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 8e4f7dfb2..391876249 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -139,6 +139,9 @@ class ImageFile(Image.Image): 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 e15cb89fc..5c911d81a 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 */