From b6c755df3204f625cb9761edea98a59d604aa153 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 31 Mar 2024 17:28:00 +0300 Subject: [PATCH 01/11] Add type hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/Image.py | 100 +++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 41981d77ce1..ccbfe2a6fd4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -55,6 +55,7 @@ _plugins, ) from ._binary import i32le, o32be, o32le +from ._typing import TypeGuard from ._util import DeferredError, is_path ElementTree: ModuleType | None @@ -120,7 +121,7 @@ class DecompressionBombError(Exception): cffi = None -def isImageType(t): +def isImageType(t: Any) -> TypeGuard[Image]: """ Checks if an object is an image object. @@ -267,7 +268,7 @@ def getmodebase(mode: str) -> str: return ImageMode.getmode(mode).basemode -def getmodetype(mode): +def getmodetype(mode: str) -> str: """ Gets the storage type mode. Given a mode, this function returns a single-layer mode suitable for storing individual bands. @@ -279,7 +280,7 @@ def getmodetype(mode): return ImageMode.getmode(mode).basetype -def getmodebandnames(mode): +def getmodebandnames(mode: str) -> tuple[str, ...]: """ Gets a list of individual band names. Given a mode, this function returns a tuple containing the names of individual bands (use @@ -311,7 +312,7 @@ def getmodebands(mode: str) -> int: _initialized = 0 -def preinit(): +def preinit() -> None: """ Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. @@ -437,7 +438,7 @@ def _getencoder(mode, encoder_name, args, extra=()): class _E: - def __init__(self, scale, offset): + def __init__(self, scale, offset) -> None: self.scale = scale self.offset = offset @@ -508,22 +509,22 @@ def __init__(self): self._exif = None @property - def width(self): + def width(self) -> int: return self.size[0] @property - def height(self): + def height(self) -> int: return self.size[1] @property - def size(self): + def size(self) -> tuple[int, int]: return self._size @property def mode(self): return self._mode - def _new(self, im): + def _new(self, im: Image) -> Image: new = Image() new.im = im new._mode = im.mode @@ -556,7 +557,7 @@ def __exit__(self, *args): self._close_fp() self.fp = None - def close(self): + def close(self) -> None: """ Closes the file pointer, if possible. @@ -589,7 +590,7 @@ def _copy(self) -> None: self.pyaccess = None self.readonly = 0 - def _ensure_mutable(self): + def _ensure_mutable(self) -> None: if self.readonly: self._copy() else: @@ -629,7 +630,7 @@ def __eq__(self, other): and self.tobytes() == other.tobytes() ) - def __repr__(self): + def __repr__(self) -> str: return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( self.__class__.__module__, self.__class__.__name__, @@ -639,7 +640,7 @@ def __repr__(self): id(self), ) - def _repr_pretty_(self, p, cycle): + def _repr_pretty_(self, p, cycle) -> None: """IPython plain text display support""" # Same as __repr__ but without unpredictable id(self), @@ -711,7 +712,7 @@ def __getstate__(self): im_data = self.tobytes() # load image first return [self.info, self.mode, self.size, self.getpalette(), im_data] - def __setstate__(self, state): + def __setstate__(self, state) -> None: Image.__init__(self) info, mode, size, palette, data = state self.info = info @@ -774,7 +775,7 @@ def tobytes(self, encoder_name: str = "raw", *args) -> bytes: return b"".join(output) - def tobitmap(self, name="image"): + def tobitmap(self, name: str = "image") -> bytes: """ Returns the image converted to an X11 bitmap. @@ -886,7 +887,12 @@ def verify(self): pass def convert( - self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 + self, + mode: str | None = None, + matrix: tuple[float, ...] | None = None, + dither: Dither | None = None, + palette: Palette = Palette.WEB, + colors: int = 256, ) -> Image: """ Returns a converted copy of this image. For the "P" mode, this @@ -1117,12 +1123,12 @@ def convert_transparency(m, v): def quantize( self, - colors=256, - method=None, - kmeans=0, + colors: int = 256, + method: Quantize | None = None, + kmeans: int = 0, palette=None, - dither=Dither.FLOYDSTEINBERG, - ): + dither: Dither = Dither.FLOYDSTEINBERG, + ) -> Image: """ Convert the image to 'P' mode with the specified number of colors. @@ -1210,7 +1216,7 @@ def copy(self) -> Image: __copy__ = copy - def crop(self, box=None) -> Image: + def crop(self, box: tuple[int, int, int, int] | None = None) -> Image: """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel @@ -1341,7 +1347,7 @@ def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: self.load() return self.im.getbbox(alpha_only) - def getcolors(self, maxcolors=256): + def getcolors(self, maxcolors: int = 256) -> list[tuple[int, int]] | None: """ Returns a list of colors used in this image. @@ -1364,7 +1370,7 @@ def getcolors(self, maxcolors=256): return out return self.im.getcolors(maxcolors) - def getdata(self, band=None): + def getdata(self, band: int | None = None) -> Image: """ Returns the contents of this image as a sequence object containing pixel values. The sequence object is flattened, so @@ -1387,7 +1393,7 @@ def getdata(self, band=None): return self.im.getband(band) return self.im # could be abused - def getextrema(self): + def getextrema(self) -> tuple[float, float] | tuple[tuple[float, float], ...]: """ Gets the minimum and maximum pixel values for each band in the image. @@ -1468,7 +1474,7 @@ def getexif(self) -> Exif: return self._exif - def _reload_exif(self): + def _reload_exif(self) -> None: if self._exif is None or not self._exif._loaded: return self._exif._loaded = False @@ -1605,7 +1611,7 @@ def getpixel(self, xy): return self.pyaccess.getpixel(xy) return self.im.getpixel(tuple(xy)) - def getprojection(self): + def getprojection(self) -> tuple[list[int], list[int]]: """ Get projection to x and y axes @@ -1617,7 +1623,7 @@ def getprojection(self): x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask=None, extrema=None) -> list[int]: + def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -2463,7 +2469,7 @@ def save(self, fp, format=None, **params) -> None: if open_fp: fp.close() - def seek(self, frame) -> None: + def seek(self, frame: int) -> None: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an @@ -2485,7 +2491,7 @@ def seek(self, frame) -> None: msg = "no more images in file" raise EOFError(msg) - def show(self, title=None): + def show(self, title: str | None = None) -> None: """ Displays this image. This method is mainly intended for debugging purposes. @@ -2526,7 +2532,7 @@ def split(self) -> tuple[Image, ...]: return (self.copy(),) return tuple(map(self._new, self.im.split())) - def getchannel(self, channel): + def getchannel(self, channel: int | str) -> Image: """ Returns an image containing a single channel of the source image. @@ -2601,13 +2607,13 @@ def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): provided_size = tuple(map(math.floor, size)) - def preserve_aspect_ratio(): + def preserve_aspect_ratio() -> tuple[int, int] | None: def round_aspect(number, key): return max(min(math.floor(number), math.ceil(number), key=key), 1) x, y = provided_size if x >= self.width and y >= self.height: - return + return None aspect = self.width / self.height if x / y >= aspect: @@ -2927,7 +2933,9 @@ def _check_size(size): return True -def new(mode, size, color=0) -> Image: +def new( + mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0 +) -> Image: """ Creates a new image with the given mode and size. @@ -3193,7 +3201,7 @@ def fromqpixmap(im): } -def _decompression_bomb_check(size): +def _decompression_bomb_check(size: tuple[int, int]) -> None: if MAX_IMAGE_PIXELS is None: return @@ -3335,7 +3343,7 @@ def _open_core(fp, filename, prefix, formats): # Image processing. -def alpha_composite(im1, im2): +def alpha_composite(im1: Image, im2: Image) -> Image: """ Alpha composite im2 over im1. @@ -3350,7 +3358,7 @@ def alpha_composite(im1, im2): return im1._new(core.alpha_composite(im1.im, im2.im)) -def blend(im1, im2, alpha): +def blend(im1: Image, im2: Image, alpha: float) -> Image: """ Creates a new image by interpolating between two input images, using a constant alpha:: @@ -3373,7 +3381,7 @@ def blend(im1, im2, alpha): return im1._new(core.blend(im1.im, im2.im, alpha)) -def composite(image1, image2, mask): +def composite(image1: Image, image2: Image, mask: Image) -> Image: """ Create composite image by blending images using a transparency mask. @@ -3483,7 +3491,7 @@ def register_save(id: str, driver) -> None: SAVE[id.upper()] = driver -def register_save_all(id, driver): +def register_save_all(id, driver) -> None: """ Registers an image function to save all the frames of a multiframe format. This function should not be @@ -3557,7 +3565,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: # Simple display support. -def _show(image, **options): +def _show(image, **options) -> None: from . import ImageShow ImageShow.show(image, **options) @@ -3613,7 +3621,7 @@ def radial_gradient(mode): # Resources -def _apply_env_variables(env=None): +def _apply_env_variables(env=None) -> None: if env is None: env = os.environ @@ -3928,13 +3936,13 @@ def get_ifd(self, tag): } return ifd - def hide_offsets(self): + def hide_offsets(self) -> None: for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): if tag in self: self._hidden_data[tag] = self[tag] del self[tag] - def __str__(self): + def __str__(self) -> str: if self._info is not None: # Load all keys into self._data for tag in self._info: @@ -3942,7 +3950,7 @@ def __str__(self): return str(self._data) - def __len__(self): + def __len__(self) -> int: keys = set(self._data) if self._info is not None: keys.update(self._info) @@ -3954,10 +3962,10 @@ def __getitem__(self, tag): del self._info[tag] return self._data[tag] - def __contains__(self, tag): + def __contains__(self, tag) -> bool: return tag in self._data or (self._info is not None and tag in self._info) - def __setitem__(self, tag, value): + def __setitem__(self, tag, value) -> None: if self._info is not None and tag in self._info: del self._info[tag] self._data[tag] = value From 008b28eb0caffb8a54fd043686cae996f0a842b6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:20:11 -0600 Subject: [PATCH 02/11] Add type hints Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ccbfe2a6fd4..b92c0bb43b8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -524,7 +524,7 @@ def size(self) -> tuple[int, int]: def mode(self): return self._mode - def _new(self, im: Image) -> Image: + def _new(self, im: Image): new = Image() new.im = im new._mode = im.mode @@ -1347,7 +1347,9 @@ def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: self.load() return self.im.getbbox(alpha_only) - def getcolors(self, maxcolors: int = 256) -> list[tuple[int, int]] | None: + def getcolors( + self, maxcolors: int = 256 + ) -> list[tuple[int, int | tuple[int, ...]]] | None: """ Returns a list of colors used in this image. @@ -1370,7 +1372,7 @@ def getcolors(self, maxcolors: int = 256) -> list[tuple[int, int]] | None: return out return self.im.getcolors(maxcolors) - def getdata(self, band: int | None = None) -> Image: + def getdata(self, band: int | None = None): """ Returns the contents of this image as a sequence object containing pixel values. The sequence object is flattened, so @@ -1393,7 +1395,7 @@ def getdata(self, band: int | None = None) -> Image: return self.im.getband(band) return self.im # could be abused - def getextrema(self) -> tuple[float, float] | tuple[tuple[float, float], ...]: + def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: """ Gets the minimum and maximum pixel values for each band in the image. From 7c5d0b9284144e785ee17cb9395063747d4ab180 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:44:27 -0600 Subject: [PATCH 03/11] Add type hints Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b92c0bb43b8..baef0aa112e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -524,7 +524,7 @@ def size(self) -> tuple[int, int]: def mode(self): return self._mode - def _new(self, im: Image): + def _new(self, im) -> Image: new = Image() new.im = im new._mode = im.mode @@ -1347,9 +1347,7 @@ def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: self.load() return self.im.getbbox(alpha_only) - def getcolors( - self, maxcolors: int = 256 - ) -> list[tuple[int, int | tuple[int, ...]]] | None: + def getcolors(self, maxcolors: int = 256): """ Returns a list of colors used in this image. From 27b10c4bd8956e8e6a5486bafe8433791dfcc2c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Mar 2024 20:38:52 +1100 Subject: [PATCH 04/11] Deprecate eval(), replacing it with lambda_eval() and unsafe_eval() --- Tests/helper.py | 4 +- Tests/test_image_reduce.py | 4 +- Tests/test_imagemath.py | 214 ------------ Tests/test_imagemath_lambda_eval.py | 498 ++++++++++++++++++++++++++++ Tests/test_imagemath_unsafe_eval.py | 221 ++++++++++++ docs/reference/ImageMath.rst | 62 +++- docs/releasenotes/10.2.0.rst | 2 +- docs/releasenotes/9.0.0.rst | 2 +- docs/releasenotes/9.0.1.rst | 2 +- src/PIL/GifImagePlugin.py | 18 +- src/PIL/ImageMath.py | 82 ++++- 11 files changed, 869 insertions(+), 240 deletions(-) delete mode 100644 Tests/test_imagemath.py create mode 100644 Tests/test_imagemath_lambda_eval.py create mode 100644 Tests/test_imagemath_unsafe_eval.py diff --git a/Tests/helper.py b/Tests/helper.py index 5d477144d2f..c1399e89bf8 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -115,7 +115,9 @@ def assert_image_similar( diff = 0 for ach, bch in zip(a.split(), b.split()): - chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert("L") + chdiff = ImageMath.lambda_eval( + lambda args: abs(args["a"] - args["b"]), a=ach, b=bch + ).convert("L") diff += sum(i * num for i, num in enumerate(chdiff.histogram())) ave_diff = diff / (a.size[0] * a.size[1]) diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index 33b33d6b7fc..fcf671daace 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -186,7 +186,9 @@ def assert_compare_images( bands = ImageMode.getmode(a.mode).bands for band, ach, bch in zip(bands, a.split(), b.split()): - ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch) + ch_diff = ImageMath.lambda_eval( + lambda args: args["convert"](abs(args["a"] - args["b"]), "L"), a=ach, b=bch + ) ch_hist = ch_diff.histogram() average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py deleted file mode 100644 index a21e2307d5f..00000000000 --- a/Tests/test_imagemath.py +++ /dev/null @@ -1,214 +0,0 @@ -from __future__ import annotations - -import pytest - -from PIL import Image, ImageMath - - -def pixel(im: Image.Image | int) -> str | int: - if isinstance(im, int): - return int(im) # hack to deal with booleans - - return f"{im.mode} {repr(im.getpixel((0, 0)))}" - - -A = Image.new("L", (1, 1), 1) -B = Image.new("L", (1, 1), 2) -Z = Image.new("L", (1, 1), 0) # Z for zero -F = Image.new("F", (1, 1), 3) -I = Image.new("I", (1, 1), 4) # noqa: E741 - -A2 = A.resize((2, 2)) -B2 = B.resize((2, 2)) - -images = {"A": A, "B": B, "F": F, "I": I} - - -def test_sanity() -> None: - assert ImageMath.eval("1") == 1 - assert ImageMath.eval("1+A", A=2) == 3 - assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3" - assert pixel(ImageMath.eval("A+B", images)) == "I 3" - assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3" - - -def test_ops() -> None: - assert pixel(ImageMath.eval("-A", images)) == "I -1" - assert pixel(ImageMath.eval("+B", images)) == "L 2" - - assert pixel(ImageMath.eval("A+B", images)) == "I 3" - assert pixel(ImageMath.eval("A-B", images)) == "I -1" - assert pixel(ImageMath.eval("A*B", images)) == "I 2" - assert pixel(ImageMath.eval("A/B", images)) == "I 0" - assert pixel(ImageMath.eval("B**2", images)) == "I 4" - assert pixel(ImageMath.eval("B**33", images)) == "I 2147483647" - - assert pixel(ImageMath.eval("float(A)+B", images)) == "F 3.0" - assert pixel(ImageMath.eval("float(A)-B", images)) == "F -1.0" - assert pixel(ImageMath.eval("float(A)*B", images)) == "F 2.0" - assert pixel(ImageMath.eval("float(A)/B", images)) == "F 0.5" - assert pixel(ImageMath.eval("float(B)**2", images)) == "F 4.0" - assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0" - - -@pytest.mark.parametrize( - "expression", - ( - "exec('pass')", - "(lambda: exec('pass'))()", - "(lambda: (lambda: exec('pass'))())()", - ), -) -def test_prevent_exec(expression: str) -> None: - with pytest.raises(ValueError): - ImageMath.eval(expression) - - -def test_prevent_double_underscores() -> None: - with pytest.raises(ValueError): - ImageMath.eval("1", {"__": None}) - - -def test_prevent_builtins() -> None: - with pytest.raises(ValueError): - ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None}) - - -def test_logical() -> None: - assert pixel(ImageMath.eval("not A", images)) == 0 - assert pixel(ImageMath.eval("A and B", images)) == "L 2" - assert pixel(ImageMath.eval("A or B", images)) == "L 1" - - -def test_convert() -> None: - assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3" - assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0" - assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" - - -def test_compare() -> None: - assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1" - assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2" - assert pixel(ImageMath.eval("A == 1", images)) == "I 1" - assert pixel(ImageMath.eval("A == 2", images)) == "I 0" - - -def test_one_image_larger() -> None: - assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3" - assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3" - - -def test_abs() -> None: - assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" - assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" - - -def test_binary_mod() -> None: - assert pixel(ImageMath.eval("A%A", A=A)) == "I 0" - assert pixel(ImageMath.eval("B%B", B=B)) == "I 0" - assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1" - assert pixel(ImageMath.eval("B%A", A=A, B=B)) == "I 0" - assert pixel(ImageMath.eval("Z%A", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0" - - -def test_bitwise_invert() -> None: - assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1" - assert pixel(ImageMath.eval("~A", A=A)) == "I -2" - assert pixel(ImageMath.eval("~B", B=B)) == "I -3" - - -def test_bitwise_and() -> None: - assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1" - - -def test_bitwise_or() -> None: - assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1" - - -def test_bitwise_xor() -> None: - assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0" - assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1" - assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0" - - -def test_bitwise_leftshift() -> None: - assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0" - assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0" - assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1" - assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2" - - -def test_bitwise_rightshift() -> None: - assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0" - assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0" - assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1" - assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0" - - -def test_logical_eq() -> None: - assert pixel(ImageMath.eval("A==A", A=A)) == "I 1" - assert pixel(ImageMath.eval("B==B", B=B)) == "I 1" - assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0" - assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0" - - -def test_logical_ne() -> None: - assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0" - assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0" - assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1" - assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1" - - -def test_logical_lt() -> None: - assert pixel(ImageMath.eval("A None: - assert pixel(ImageMath.eval("A<=A", A=A)) == "I 1" - assert pixel(ImageMath.eval("B<=B", B=B)) == "I 1" - assert pixel(ImageMath.eval("A<=B", A=A, B=B)) == "I 1" - assert pixel(ImageMath.eval("B<=A", A=A, B=B)) == "I 0" - - -def test_logical_gt() -> None: - assert pixel(ImageMath.eval("A>A", A=A)) == "I 0" - assert pixel(ImageMath.eval("B>B", B=B)) == "I 0" - assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0" - assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1" - - -def test_logical_ge() -> None: - assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1" - assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1" - assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0" - assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1" - - -def test_logical_equal() -> None: - assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1" - assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1" - assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1" - assert pixel(ImageMath.eval("equal(A, B)", A=A, B=B)) == "I 0" - assert pixel(ImageMath.eval("equal(B, A)", A=A, B=B)) == "I 0" - assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0" - - -def test_logical_not_equal() -> None: - assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0" - assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0" - assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0" - assert pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)) == "I 1" - assert pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)) == "I 1" - assert pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)) == "I 1" diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py new file mode 100644 index 00000000000..3915e64aae1 --- /dev/null +++ b/Tests/test_imagemath_lambda_eval.py @@ -0,0 +1,498 @@ +from __future__ import annotations + +import pytest + +from PIL import Image, ImageMath + + +def pixel(im: Image.Image | int) -> str | int: + if isinstance(im, int): + return int(im) # hack to deal with booleans + + return f"{im.mode} {repr(im.getpixel((0, 0)))}" + + +A = Image.new("L", (1, 1), 1) +B = Image.new("L", (1, 1), 2) +Z = Image.new("L", (1, 1), 0) # Z for zero +F = Image.new("F", (1, 1), 3) +I = Image.new("I", (1, 1), 4) # noqa: E741 + +A2 = A.resize((2, 2)) +B2 = B.resize((2, 2)) + +images = {"A": A, "B": B, "F": F, "I": I} + + +def test_sanity() -> None: + assert ImageMath.lambda_eval(lambda args: 1) == 1 + assert ImageMath.lambda_eval(lambda args: 1 + args["A"], A=2) == 3 + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B)) + == "I 3" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + == "I 3" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) + args["B"], images + ) + ) + == "F 3.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["int"](args["float"](args["A"]) + args["B"]), images + ) + ) + == "I 3" + ) + + +def test_ops() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1" + + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images)) + == "I 3" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images)) + == "I -1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images)) + == "I 2" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images)) + == "I 0" + ) + assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4" + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images)) + == "I 2147483647" + ) + + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) + args["B"], images + ) + ) + == "F 3.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) - args["B"], images + ) + ) + == "F -1.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) * args["B"], images + ) + ) + == "F 2.0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["float"](args["A"]) / args["B"], images + ) + ) + == "F 0.5" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images)) + == "F 4.0" + ) + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images) + ) + == "F 8589934592.0" + ) + + +def test_logical() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0 + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images)) + == "L 2" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images)) + == "L 1" + ) + + +def test_convert() -> None: + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["convert"](args["A"] + args["B"], "L"), images + ) + ) + == "L 3" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["convert"](args["A"] + args["B"], "1"), images + ) + ) + == "1 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["convert"](args["A"] + args["B"], "RGB"), images + ) + ) + == "RGB (3, 3, 3)" + ) + + +def test_compare() -> None: + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["min"](args["A"], args["B"]), images + ) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["max"](args["A"], args["B"]), images + ) + ) + == "I 2" + ) + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0" + + +def test_one_image_larger() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A2, B=B)) + == "I 3" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], A=A, B=B2)) + == "I 3" + ) + + +def test_abs() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: abs(args["A"]), A=A)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: abs(args["B"]), B=B)) == "I 2" + + +def test_binary_mod() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] % args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] % args["A"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["A"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] % args["B"], B=B, Z=Z)) + == "I 0" + ) + + +def test_bitwise_invert() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: ~args["Z"], Z=Z)) == "I -1" + assert pixel(ImageMath.lambda_eval(lambda args: ~args["A"], A=A)) == "I -2" + assert pixel(ImageMath.lambda_eval(lambda args: ~args["B"], B=B)) == "I -3" + + +def test_bitwise_and() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] & args["A"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] & args["A"], A=A, Z=Z)) + == "I 1" + ) + + +def test_bitwise_or() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] | args["A"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["Z"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] | args["A"], A=A, Z=Z)) + == "I 1" + ) + + +def test_bitwise_xor() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["Z"], A=A, Z=Z)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["Z"] ^ args["A"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["Z"], A=A, Z=Z)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] ^ args["A"], A=A, Z=Z)) + == "I 0" + ) + + +def test_bitwise_leftshift() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 0, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] << 1, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 0, A=A)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] << 1, A=A)) == "I 2" + + +def test_bitwise_rightshift() -> None: + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 0, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["Z"] >> 1, Z=Z)) == "I 0" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 0, A=A)) == "I 1" + assert pixel(ImageMath.lambda_eval(lambda args: args["A"] >> 1, A=A)) == "I 0" + + +def test_logical_eq() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["A"], A=A)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["B"], B=B)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] == args["B"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] == args["A"], A=A, B=B)) + == "I 0" + ) + + +def test_logical_ne() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] != args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] != args["A"], A=A, B=B)) + == "I 1" + ) + + +def test_logical_lt() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] < args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] < args["A"], A=A, B=B)) + == "I 0" + ) + + +def test_logical_le() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["A"], A=A)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["B"], B=B)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] <= args["B"], A=A, B=B)) + == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] <= args["A"], A=A, B=B)) + == "I 0" + ) + + +def test_logical_gt() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["A"], A=A)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["B"], B=B)) == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] > args["B"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] > args["A"], A=A, B=B)) + == "I 1" + ) + + +def test_logical_ge() -> None: + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["A"], A=A)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["B"], B=B)) == "I 1" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["A"] >= args["B"], A=A, B=B)) + == "I 0" + ) + assert ( + pixel(ImageMath.lambda_eval(lambda args: args["B"] >= args["A"], A=A, B=B)) + == "I 1" + ) + + +def test_logical_equal() -> None: + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["equal"](args["A"], args["A"]), A=A) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["equal"](args["B"], args["B"]), B=B) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval(lambda args: args["equal"](args["Z"], args["Z"]), Z=Z) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["equal"](args["A"], args["B"]), A=A, B=B + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["equal"](args["B"], args["A"]), A=A, B=B + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["equal"](args["A"], args["Z"]), A=A, Z=Z + ) + ) + == "I 0" + ) + + +def test_logical_not_equal() -> None: + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["A"], args["A"]), A=A + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["B"], args["B"]), B=B + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["Z"], args["Z"]), Z=Z + ) + ) + == "I 0" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["A"], args["B"]), A=A, B=B + ) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["B"], args["A"]), A=A, B=B + ) + ) + == "I 1" + ) + assert ( + pixel( + ImageMath.lambda_eval( + lambda args: args["notequal"](args["A"], args["Z"]), A=A, Z=Z + ) + ) + == "I 1" + ) diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py new file mode 100644 index 00000000000..7b8a562d739 --- /dev/null +++ b/Tests/test_imagemath_unsafe_eval.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import pytest + +from PIL import Image, ImageMath + + +def pixel(im: Image.Image | int) -> str | int: + if isinstance(im, int): + return int(im) # hack to deal with booleans + + return f"{im.mode} {repr(im.getpixel((0, 0)))}" + + +A = Image.new("L", (1, 1), 1) +B = Image.new("L", (1, 1), 2) +Z = Image.new("L", (1, 1), 0) # Z for zero +F = Image.new("F", (1, 1), 3) +I = Image.new("I", (1, 1), 4) # noqa: E741 + +A2 = A.resize((2, 2)) +B2 = B.resize((2, 2)) + +images = {"A": A, "B": B, "F": F, "I": I} + + +def test_sanity() -> None: + assert ImageMath.unsafe_eval("1") == 1 + assert ImageMath.unsafe_eval("1+A", A=2) == 3 + assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3" + + +def test_eval_deprecated() -> None: + with pytest.warns(DeprecationWarning): + assert ImageMath.eval("1") == 1 + + +def test_ops() -> None: + assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2" + + assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1" + assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4" + assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647" + + assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0" + assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0" + assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0" + assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5" + assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0" + assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0" + + +@pytest.mark.parametrize( + "expression", + ( + "exec('pass')", + "(lambda: exec('pass'))()", + "(lambda: (lambda: exec('pass'))())()", + ), +) +def test_prevent_exec(expression: str) -> None: + with pytest.raises(ValueError): + ImageMath.unsafe_eval(expression) + + +def test_prevent_double_underscores() -> None: + with pytest.raises(ValueError): + ImageMath.unsafe_eval("1", {"__": None}) + + +def test_prevent_builtins() -> None: + with pytest.raises(ValueError): + ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None}) + + +def test_logical() -> None: + assert pixel(ImageMath.unsafe_eval("not A", images)) == 0 + assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2" + assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1" + + +def test_convert() -> None: + assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3" + assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0" + assert ( + pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" + ) + + +def test_compare() -> None: + assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2" + assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0" + + +def test_one_image_larger() -> None: + assert pixel(ImageMath.unsafe_eval("A+B", A=A2, B=B)) == "I 3" + assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B2)) == "I 3" + + +def test_abs() -> None: + assert pixel(ImageMath.unsafe_eval("abs(A)", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("abs(B)", B=B)) == "I 2" + + +def test_binary_mod() -> None: + assert pixel(ImageMath.unsafe_eval("A%A", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B%B", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A%B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B%A", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z%A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z%B", B=B, Z=Z)) == "I 0" + + +def test_bitwise_invert() -> None: + assert pixel(ImageMath.unsafe_eval("~Z", Z=Z)) == "I -1" + assert pixel(ImageMath.unsafe_eval("~A", A=A)) == "I -2" + assert pixel(ImageMath.unsafe_eval("~B", B=B)) == "I -3" + + +def test_bitwise_and() -> None: + assert pixel(ImageMath.unsafe_eval("Z&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z&A", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A&Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A&A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_or() -> None: + assert pixel(ImageMath.unsafe_eval("Z|Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z|A", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A|Z", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A|A", A=A, Z=Z)) == "I 1" + + +def test_bitwise_xor() -> None: + assert pixel(ImageMath.unsafe_eval("Z^Z", A=A, Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z^A", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A^Z", A=A, Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A^A", A=A, Z=Z)) == "I 0" + + +def test_bitwise_leftshift() -> None: + assert pixel(ImageMath.unsafe_eval("Z<<0", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z<<1", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A<<0", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A<<1", A=A)) == "I 2" + + +def test_bitwise_rightshift() -> None: + assert pixel(ImageMath.unsafe_eval("Z>>0", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("Z>>1", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A>>0", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A>>1", A=A)) == "I 0" + + +def test_logical_eq() -> None: + assert pixel(ImageMath.unsafe_eval("A==A", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B==B", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A==B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B==A", A=A, B=B)) == "I 0" + + +def test_logical_ne() -> None: + assert pixel(ImageMath.unsafe_eval("A!=A", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B!=B", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A!=B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B!=A", A=A, B=B)) == "I 1" + + +def test_logical_lt() -> None: + assert pixel(ImageMath.unsafe_eval("A None: + assert pixel(ImageMath.unsafe_eval("A<=A", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B<=B", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A<=B", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B<=A", A=A, B=B)) == "I 0" + + +def test_logical_gt() -> None: + assert pixel(ImageMath.unsafe_eval("A>A", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B>B", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("A>B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B>A", A=A, B=B)) == "I 1" + + +def test_logical_ge() -> None: + assert pixel(ImageMath.unsafe_eval("A>=A", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("B>=B", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("A>=B", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("B>=A", A=A, B=B)) == "I 1" + + +def test_logical_equal() -> None: + assert pixel(ImageMath.unsafe_eval("equal(A, A)", A=A)) == "I 1" + assert pixel(ImageMath.unsafe_eval("equal(B, B)", B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("equal(Z, Z)", Z=Z)) == "I 1" + assert pixel(ImageMath.unsafe_eval("equal(A, B)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("equal(B, A)", A=A, B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("equal(A, Z)", A=A, Z=Z)) == "I 0" + + +def test_logical_not_equal() -> None: + assert pixel(ImageMath.unsafe_eval("notequal(A, A)", A=A)) == "I 0" + assert pixel(ImageMath.unsafe_eval("notequal(B, B)", B=B)) == "I 0" + assert pixel(ImageMath.unsafe_eval("notequal(Z, Z)", Z=Z)) == "I 0" + assert pixel(ImageMath.unsafe_eval("notequal(A, B)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("notequal(B, A)", A=A, B=B)) == "I 1" + assert pixel(ImageMath.unsafe_eval("notequal(A, Z)", A=A, Z=Z)) == "I 1" diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index ee07efa0132..026c7cd9e07 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -4,9 +4,12 @@ :py:mod:`~PIL.ImageMath` Module =============================== -The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”. The -module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes -an expression string and one or more images. +The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that +can take a number of images and generate a result. + +In the current version, :py:mod:`~PIL.ImageMath` only supports single-layer images. To +process multi-band images, use the :py:meth:`~PIL.Image.Image.split` method or +:py:func:`~PIL.Image.merge` function. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- @@ -17,13 +20,36 @@ Example: Using the :py:mod:`~PIL.ImageMath` module with Image.open("image1.jpg") as im1: with Image.open("image2.jpg") as im2: + out = ImageMath.lambda_eval( + lambda args: args["convert"](args["min"](args["a"], args["b"]), 'L'), + a=im1, + b=im2 + ) + out = ImageMath.unsafe_eval( + "convert(min(a, b), 'L')", + a=im1, + b=im2 + ) + +.. py:function:: lambda_eval(expression, environment) + + Returns the result of an image function. + + :param expression: A function that receives a dictionary. + :param options: Values to add to the function's dictionary, mapping image + names to Image instances. You can use one or more keyword + arguments instead of a dictionary, as shown in the above + example. Note that the names must be valid Python + identifiers. + :return: An image, an integer value, a floating point value, + or a pixel tuple, depending on the expression. - out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2) - out.save("result.png") - -.. py:function:: eval(expression, environment) +.. py:function:: unsafe_eval(expression, environment) - Evaluate expression in the given environment. + Evaluates an image expression. This uses Python's ``eval()`` function to process + the expression string, and carries the security risks of doing so. It is not + recommended to process expressions without considering this. + :py:meth:`~lambda_eval` is a more secure alternative. In the current version, :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band images, use the @@ -33,19 +59,25 @@ Example: Using the :py:mod:`~PIL.ImageMath` module :param expression: A string which uses the standard Python expression syntax. In addition to the standard operators, you can also use the functions described below. - :param environment: A dictionary that maps image names to Image instances. - You can use one or more keyword arguments instead of a - dictionary, as shown in the above example. Note that - the names must be valid Python identifiers. + :param options: Values to add to the function's dictionary, mapping image + names to Image instances. You can use one or more keyword + arguments instead of a dictionary, as shown in the above + example. Note that the names must be valid Python + identifiers. :return: An image, an integer value, a floating point value, or a pixel tuple, depending on the expression. Expression syntax ----------------- -Expressions are standard Python expressions, but they’re evaluated in a -non-standard environment. You can use PIL methods as usual, plus the following -set of operators and functions: +:py:meth:`~lambda_eval` expressions are functions that receive a dictionary containing +images and operators. + +:py:meth:`~unsafe_eval` expressions are standard Python expressions, but they’re +evaluated in a non-standard environment. + +In both cases, you can use Pillow methods as usual, plus the following set of operators +and functions. Standard Operators ^^^^^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 0ffad2e8a1c..1c6b78b0841 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -29,7 +29,7 @@ they do not extend beyond the bitmap image. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If an attacker has control over the keys passed to the -``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute +``environment`` argument of :py:meth:`!PIL.ImageMath.eval`, they may be able to execute arbitrary code. To prevent this, keys matching the names of builtins and keys containing double underscores will now raise a :py:exc:`ValueError`. diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index 8d59aef3029..fee66b6d0b5 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -47,7 +47,7 @@ Google's `OSS-Fuzz`_ project for finding this issue. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To limit :py:class:`PIL.ImageMath` to working with images, Pillow -will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will +will now restrict the builtins available to :py:meth:`!PIL.ImageMath.eval`. This will help prevent problems arising if users evaluate arbitrary expressions, such as ``ImageMath.eval("exec(exit())")``. diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index a25e3f5ac66..f65e3bcc2ec 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -18,7 +18,7 @@ has been present since PIL. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ While Pillow 9.0 restricted top-level builtins available to -:py:meth:`PIL.ImageMath.eval`, it did not prevent builtins +:py:meth:`!PIL.ImageMath.eval`, it did not prevent builtins available to lambda expressions. These are now also restricted. Other Changes diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b8671068d59..6b415d2384a 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -652,8 +652,17 @@ def _write_multiple_frames(im, fp, palette): fill = Image.new("P", delta.size, encoderinfo["transparency"]) if delta.mode == "RGBA": r, g, b, a = delta.split() - mask = ImageMath.eval( - "convert(max(max(max(r, g), b), a) * 255, '1')", + mask = ImageMath.lambda_eval( + lambda args: args["convert"]( + args["max"]( + args["max"]( + args["max"](args["r"], args["g"]), args["b"] + ), + args["a"], + ) + * 255, + "1", + ), r=r, g=g, b=b, @@ -665,7 +674,10 @@ def _write_multiple_frames(im, fp, palette): delta_l = Image.new("L", delta.size) delta_l.putdata(delta.getdata()) delta = delta_l - mask = ImageMath.eval("convert(im * 255, '1')", im=delta) + mask = ImageMath.lambda_eval( + lambda args: args["convert"](args["im"] * 255, "1"), + im=delta, + ) diff_frame.paste(fill, mask=ImageOps.invert(mask)) else: bbox = None diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index a7652f237ed..5ed9e08ad3f 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -18,9 +18,10 @@ import builtins from types import CodeType -from typing import Any +from typing import Any, Callable from . import Image, _imagingmath +from ._deprecate import deprecate class _Operand: @@ -235,9 +236,55 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: } -def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: +def lambda_eval(expression: Callable[[dict[str, Any]], Any], + _dict: dict[str, Any] = {}, + **kw: Any,) -> Any: """ - Evaluates an image expression. + Returns the result of an image function. + + In the current version, :py:mod:`~PIL.ImageMath` only supports + single-layer images. To process multi-band images, use the + :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` + function. + + :param expression: A function that receives a dictionary. + :param options: Values to add to the function's dictionary. You + can either use a dictionary, or one or more keyword + arguments. + :return: The expression result. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + """ + + args: dict[str, Any] = ops.copy() + args.update(_dict) + args.update(kw) + for k, v in args.items(): + if hasattr(v, "im"): + args[k] = _Operand(v) + + out = expression(args) + try: + return out.im + except AttributeError: + return out + + +def unsafe_eval( + expression: str, + _dict: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Evaluates an image expression. This uses Python's ``eval()`` function to process + the expression string, and carries the security risks of doing so. It is not + recommended to process expressions without considering this. + :py:meth:`~lambda_eval` is a more secure alternative. + + In the current version, :py:mod:`~PIL.ImageMath` only supports + single-layer images. To process multi-band images, use the + :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` + function. :param expression: A string containing a Python-style expression. :param options: Values to add to the evaluation context. You @@ -279,3 +326,32 @@ def scan(code: CodeType) -> None: return out.im except AttributeError: return out + + +def eval( + expression: str, + _dict: dict[str, Any] = {}, + **kw: Any, +) -> Any: + """ + Evaluates an image expression. + + Deprecated. Use lambda_eval() or unsafe_eval() instead. + + :param expression: A string containing a Python-style expression. + :param options: Values to add to the evaluation context. You + can either use a dictionary, or one or more keyword + arguments. + :return: The evaluated expression. This is usually an image object, but can + also be an integer, a floating point value, or a pixel tuple, + depending on the expression. + + .. deprecated:: 10.3.0 + """ + + deprecate( + "ImageMath.eval", + 12, + "ImageMath.lambda_eval or ImageMath.unsafe_eval", + ) + return unsafe_eval(expression, _dict, **kw) From a7e82cbe9a2ecc17311d4d69091992a741360677 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Mar 2024 05:50:08 +1100 Subject: [PATCH 05/11] Removed "In the current version" --- docs/reference/ImageMath.rst | 13 ++++++------- src/PIL/ImageMath.py | 14 ++++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 026c7cd9e07..5fd61f881f7 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -7,9 +7,9 @@ The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that can take a number of images and generate a result. -In the current version, :py:mod:`~PIL.ImageMath` only supports single-layer images. To -process multi-band images, use the :py:meth:`~PIL.Image.Image.split` method or -:py:func:`~PIL.Image.merge` function. +:py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band +images, use the :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` +function. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- @@ -51,10 +51,9 @@ Example: Using the :py:mod:`~PIL.ImageMath` module recommended to process expressions without considering this. :py:meth:`~lambda_eval` is a more secure alternative. - In the current version, :py:mod:`~PIL.ImageMath` only supports - single-layer images. To process multi-band images, use the - :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` - function. + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. :param expression: A string which uses the standard Python expression syntax. In addition to the standard operators, you can diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 5ed9e08ad3f..2ffebbe71c5 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -242,10 +242,9 @@ def lambda_eval(expression: Callable[[dict[str, Any]], Any], """ Returns the result of an image function. - In the current version, :py:mod:`~PIL.ImageMath` only supports - single-layer images. To process multi-band images, use the - :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` - function. + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. :param expression: A function that receives a dictionary. :param options: Values to add to the function's dictionary. You @@ -281,10 +280,9 @@ def unsafe_eval( recommended to process expressions without considering this. :py:meth:`~lambda_eval` is a more secure alternative. - In the current version, :py:mod:`~PIL.ImageMath` only supports - single-layer images. To process multi-band images, use the - :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` - function. + :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band + images, use the :py:meth:`~PIL.Image.Image.split` method or + :py:func:`~PIL.Image.merge` function. :param expression: A string containing a Python-style expression. :param options: Values to add to the evaluation context. You From f932cb895fcfa369461e6829230a0e22dace8a31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Mar 2024 06:16:09 +1100 Subject: [PATCH 06/11] Added danger alerts --- docs/reference/ImageMath.rst | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 5fd61f881f7..703b2f5b943 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -46,10 +46,13 @@ Example: Using the :py:mod:`~PIL.ImageMath` module .. py:function:: unsafe_eval(expression, environment) - Evaluates an image expression. This uses Python's ``eval()`` function to process - the expression string, and carries the security risks of doing so. It is not - recommended to process expressions without considering this. - :py:meth:`~lambda_eval` is a more secure alternative. + Evaluates an image expression. + + .. danger:: + This uses Python's ``eval()`` function to process the expression string, + and carries the security risks of doing so. It is not + recommended to process expressions without considering this. + :py:meth:`lambda_eval` is a more secure alternative. :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band images, use the :py:meth:`~PIL.Image.Image.split` method or @@ -69,14 +72,17 @@ Example: Using the :py:mod:`~PIL.ImageMath` module Expression syntax ----------------- -:py:meth:`~lambda_eval` expressions are functions that receive a dictionary containing -images and operators. +* :py:meth:`lambda_eval` expressions are functions that receive a dictionary + containing images and operators. -:py:meth:`~unsafe_eval` expressions are standard Python expressions, but they’re -evaluated in a non-standard environment. +* :py:meth:`unsafe_eval` expressions are standard Python expressions, + but they’re evaluated in a non-standard environment. -In both cases, you can use Pillow methods as usual, plus the following set of operators -and functions. +.. danger:: + :py:meth:`unsafe_eval` uses Python's ``eval()`` function to process the + expression string, and carries the security risks of doing so. + It is not recommended to process expressions without considering this. + :py:meth:`lambda_eval` is a more secure alternative. Standard Operators ^^^^^^^^^^^^^^^^^^ From f6596d529399a80fb89daf2675987f1fd92cbd64 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 Mar 2024 06:32:42 +1100 Subject: [PATCH 07/11] Added release notes and document deprecation --- docs/deprecations.rst | 8 ++++++++ docs/releasenotes/10.3.0.rst | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 33bc141877f..c3d1ba4f028 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -92,6 +92,14 @@ Deprecated Use instead :py:data:`sys.version_info`, and ``PIL.__version__`` ============================================ ==================================================== +ImageMath eval() +^^^^^^^^^^^^^^^^ + +.. deprecated:: 10.3.0 + +``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. + Removed features ---------------- diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index a73efcee418..607f0b2620a 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -4,10 +4,15 @@ Security ======== -TODO -^^^^ +ImageMath eval() +^^^^^^^^^^^^^^^^ -TODO +.. danger:: + ``ImageMath.eval()`` uses Python's ``eval()`` function to process the expression + string, and carries the security risks of doing so. A direct replacement for this is + the new :py:meth:`~PIL.ImageMath.unsafe_eval`, but that carries the same risks. It is + not recommended to process expressions without considering this. + :py:meth:`~PIL.ImageMath.lambda_eval` is a more secure alternative. :cve:`YYYY-XXXXX`: TODO ^^^^^^^^^^^^^^^^^^^^^^^ @@ -58,6 +63,13 @@ Deprecated Use instead :py:data:`sys.version_info`, and ``PIL.__version__`` ============================================ ==================================================== +ImageMath.eval() +^^^^^^^^^^^^^^^^ + +``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or +:py:meth:`~PIL.ImageMath.unsafe_eval` instead. See earlier security notes for more +information. + API Changes =========== From 8f3860c29ba4b3de97fa1b22dad6b123bf2f7bad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 06:17:00 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_imagemath_lambda_eval.py | 2 -- src/PIL/ImageMath.py | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py index 3915e64aae1..5769c903e49 100644 --- a/Tests/test_imagemath_lambda_eval.py +++ b/Tests/test_imagemath_lambda_eval.py @@ -1,7 +1,5 @@ from __future__ import annotations -import pytest - from PIL import Image, ImageMath diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 2ffebbe71c5..5d83929dc0a 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -236,9 +236,11 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: } -def lambda_eval(expression: Callable[[dict[str, Any]], Any], +def lambda_eval( + expression: Callable[[dict[str, Any]], Any], _dict: dict[str, Any] = {}, - **kw: Any,) -> Any: + **kw: Any, +) -> Any: """ Returns the result of an image function. From a670597bc30e9d489656fc9d807170b8f3d7ca57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Apr 2024 17:21:04 +1100 Subject: [PATCH 09/11] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fd4a27d869b..5d08940ca3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Raise ValueError if seeking to greater than offset-sized integer in TIFF #7883 + [radarhere] + - Add --report argument to __main__.py to omit supported formats #7818 [nulano, radarhere, hugovk] From f5eeeacf7539eaa0d93a677d7666bc7c142c8d1c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:35:49 +0300 Subject: [PATCH 10/11] Name as 'options' in lambda_eval and unsafe_eval, but '_dict' in deprecated eval --- src/PIL/ImageMath.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 5d83929dc0a..77472a24c68 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -238,7 +238,7 @@ def imagemath_convert(self: _Operand, mode: str) -> _Operand: def lambda_eval( expression: Callable[[dict[str, Any]], Any], - _dict: dict[str, Any] = {}, + options: dict[str, Any] = {}, **kw: Any, ) -> Any: """ @@ -258,7 +258,7 @@ def lambda_eval( """ args: dict[str, Any] = ops.copy() - args.update(_dict) + args.update(options) args.update(kw) for k, v in args.items(): if hasattr(v, "im"): @@ -273,7 +273,7 @@ def lambda_eval( def unsafe_eval( expression: str, - _dict: dict[str, Any] = {}, + options: dict[str, Any] = {}, **kw: Any, ) -> Any: """ @@ -297,12 +297,12 @@ def unsafe_eval( # build execution namespace args: dict[str, Any] = ops.copy() - for k in list(_dict.keys()) + list(kw.keys()): + for k in list(options.keys()) + list(kw.keys()): if "__" in k or hasattr(builtins, k): msg = f"'{k}' not allowed" raise ValueError(msg) - args.update(_dict) + args.update(options) args.update(kw) for k, v in args.items(): if hasattr(v, "im"): @@ -339,9 +339,9 @@ def eval( Deprecated. Use lambda_eval() or unsafe_eval() instead. :param expression: A string containing a Python-style expression. - :param options: Values to add to the evaluation context. You - can either use a dictionary, or one or more keyword - arguments. + :param _dict: Values to add to the evaluation context. You + can either use a dictionary, or one or more keyword + arguments. :return: The evaluated expression. This is usually an image object, but can also be an integer, a floating point value, or a pixel tuple, depending on the expression. From 5beb0b66648db8b542bb5260eed79b25e33d643b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 Apr 2024 19:24:44 +1100 Subject: [PATCH 11/11] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5d08940ca3b..d314ac4e96b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Deprecate eval(), replacing it with lambda_eval() and unsafe_eval() #7927 + [radarhere, hugovk] + - Raise ValueError if seeking to greater than offset-sized integer in TIFF #7883 [radarhere]