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)