From 27b10c4bd8956e8e6a5486bafe8433791dfcc2c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Mar 2024 20:38:52 +1100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] [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 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 6/6] 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.