diff --git a/CHANGES.rst b/CHANGES.rst
index fd4a27d869b..d314ac4e96b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,12 @@ 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]
+
- Add --report argument to __main__.py to omit supported formats #7818
[nulano, radarhere, hugovk]
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..5769c903e49
--- /dev/null
+++ b/Tests/test_imagemath_lambda_eval.py
@@ -0,0 +1,496 @@
+from __future__ import annotations
+
+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/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/reference/ImageMath.rst b/docs/reference/ImageMath.rst
index ee07efa0132..703b2f5b943 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.
+
+: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,35 +20,69 @@ 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:: unsafe_eval(expression, environment)
-.. py:function:: eval(expression, environment)
+ Evaluates an image expression.
- Evaluate expression in the given environment.
+ .. 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.
- 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
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.
+
+.. 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
^^^^^^^^^^^^^^^^^^
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/10.3.0.rst b/docs/releasenotes/10.3.0.rst
index e5a47b28142..2f0437d94f9 100644
--- a/docs/releasenotes/10.3.0.rst
+++ b/docs/releasenotes/10.3.0.rst
@@ -4,6 +4,16 @@
Security
========
+ImageMath eval()
+^^^^^^^^^^^^^^^^
+
+.. 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:`2024-28219`: Fix buffer overflow in ``_imagingcms.c``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -48,6 +58,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
===========
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/Image.py b/src/PIL/Image.py
index 41981d77ce1..baef0aa112e 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:
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):
"""
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):
"""
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[int, int], ...]:
"""
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
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index a7652f237ed..77472a24c68 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],
+ options: dict[str, Any] = {},
+ **kw: Any,
+) -> Any:
"""
- Evaluates an image expression.
+ Returns the result of an image 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
+ 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(options)
+ 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,
+ options: 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.
+
+ :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
@@ -250,12 +297,12 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any:
# 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"):
@@ -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 _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.
+
+ .. deprecated:: 10.3.0
+ """
+
+ deprecate(
+ "ImageMath.eval",
+ 12,
+ "ImageMath.lambda_eval or ImageMath.unsafe_eval",
+ )
+ return unsafe_eval(expression, _dict, **kw)