Merge branch 'main' into lcms

This commit is contained in:
Andrew Murray 2024-04-01 19:26:55 +11:00 committed by GitHub
commit aeb51cbb16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 962 additions and 291 deletions

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
10.3.0 (unreleased) 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 - Add --report argument to __main__.py to omit supported formats #7818
[nulano, radarhere, hugovk] [nulano, radarhere, hugovk]

View File

@ -115,7 +115,9 @@ def assert_image_similar(
diff = 0 diff = 0
for ach, bch in zip(a.split(), b.split()): 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())) diff += sum(i * num for i, num in enumerate(chdiff.histogram()))
ave_diff = diff / (a.size[0] * a.size[1]) ave_diff = diff / (a.size[0] * a.size[1])

View File

@ -186,7 +186,9 @@ def assert_compare_images(
bands = ImageMode.getmode(a.mode).bands bands = ImageMode.getmode(a.mode).bands
for band, ach, bch in zip(bands, a.split(), b.split()): 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() ch_hist = ch_diff.histogram()
average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( average_diff = sum(i * num for i, num in enumerate(ch_hist)) / (

View File

@ -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<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"
def test_logical_le() -> 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"

View File

@ -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"
)

View File

@ -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<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"
def test_logical_le() -> 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"

View File

@ -92,6 +92,14 @@ Deprecated Use instead
:py:data:`sys.version_info`, and ``PIL.__version__`` :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 Removed features
---------------- ----------------

View File

@ -4,9 +4,12 @@
:py:mod:`~PIL.ImageMath` Module :py:mod:`~PIL.ImageMath` Module
=============================== ===============================
The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”. The The :py:mod:`~PIL.ImageMath` module can be used to evaluate “image expressions”, that
module provides a single :py:meth:`~PIL.ImageMath.eval` function, which takes can take a number of images and generate a result.
an expression string and one or more images.
: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 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("image1.jpg") as im1:
with Image.open("image2.jpg") as im2: 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
)
out = ImageMath.eval("convert(min(a, b), 'L')", a=im1, b=im2) .. py:function:: lambda_eval(expression, environment)
out.save("result.png")
.. py:function:: eval(expression, environment) Returns the result of an image function.
Evaluate expression in the given environment. :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.
In the current version, :py:mod:`~PIL.ImageMath` only supports .. py:function:: unsafe_eval(expression, environment)
single-layer images. To process multi-band images, use the
:py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` Evaluates an image expression.
function.
.. 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
:py:func:`~PIL.Image.merge` function.
:param expression: A string which uses the standard Python expression :param expression: A string which uses the standard Python expression
syntax. In addition to the standard operators, you can syntax. In addition to the standard operators, you can
also use the functions described below. also use the functions described below.
:param environment: A dictionary that maps image names to Image instances. :param options: Values to add to the function's dictionary, mapping image
You can use one or more keyword arguments instead of a names to Image instances. You can use one or more keyword
dictionary, as shown in the above example. Note that arguments instead of a dictionary, as shown in the above
the names must be valid Python identifiers. example. Note that the names must be valid Python
identifiers.
:return: An image, an integer value, a floating point value, :return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression. or a pixel tuple, depending on the expression.
Expression syntax Expression syntax
----------------- -----------------
Expressions are standard Python expressions, but theyre evaluated in a * :py:meth:`lambda_eval` expressions are functions that receive a dictionary
non-standard environment. You can use PIL methods as usual, plus the following containing images and operators.
set of operators and functions:
* :py:meth:`unsafe_eval` expressions are standard Python expressions,
but theyre 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 Standard Operators
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^

View File

@ -29,7 +29,7 @@ they do not extend beyond the bitmap image.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If an attacker has control over the keys passed to the 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 arbitrary code. To prevent this, keys matching the names of builtins and keys
containing double underscores will now raise a :py:exc:`ValueError`. containing double underscores will now raise a :py:exc:`ValueError`.

View File

@ -4,6 +4,16 @@
Security 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`` :cve:`2024-28219`: Fix buffer overflow in ``_imagingcms.c``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -48,6 +58,13 @@ Deprecated Use instead
:py:data:`sys.version_info`, and ``PIL.__version__`` :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 API Changes
=========== ===========

View File

@ -47,7 +47,7 @@ Google's `OSS-Fuzz`_ project for finding this issue.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To limit :py:class:`PIL.ImageMath` to working with images, Pillow 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 help prevent problems arising if users evaluate arbitrary expressions, such as
``ImageMath.eval("exec(exit())")``. ``ImageMath.eval("exec(exit())")``.

View File

@ -18,7 +18,7 @@ has been present since PIL.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
While Pillow 9.0 restricted top-level builtins available to 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. available to lambda expressions. These are now also restricted.
Other Changes Other Changes

View File

@ -652,8 +652,17 @@ def _write_multiple_frames(im, fp, palette):
fill = Image.new("P", delta.size, encoderinfo["transparency"]) fill = Image.new("P", delta.size, encoderinfo["transparency"])
if delta.mode == "RGBA": if delta.mode == "RGBA":
r, g, b, a = delta.split() r, g, b, a = delta.split()
mask = ImageMath.eval( mask = ImageMath.lambda_eval(
"convert(max(max(max(r, g), b), a) * 255, '1')", lambda args: args["convert"](
args["max"](
args["max"](
args["max"](args["r"], args["g"]), args["b"]
),
args["a"],
)
* 255,
"1",
),
r=r, r=r,
g=g, g=g,
b=b, b=b,
@ -665,7 +674,10 @@ def _write_multiple_frames(im, fp, palette):
delta_l = Image.new("L", delta.size) delta_l = Image.new("L", delta.size)
delta_l.putdata(delta.getdata()) delta_l.putdata(delta.getdata())
delta = delta_l 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)) diff_frame.paste(fill, mask=ImageOps.invert(mask))
else: else:
bbox = None bbox = None

View File

@ -55,6 +55,7 @@ from . import (
_plugins, _plugins,
) )
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._typing import TypeGuard
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
ElementTree: ModuleType | None ElementTree: ModuleType | None
@ -120,7 +121,7 @@ except ImportError:
cffi = None cffi = None
def isImageType(t): def isImageType(t: Any) -> TypeGuard[Image]:
""" """
Checks if an object is an image object. Checks if an object is an image object.
@ -267,7 +268,7 @@ def getmodebase(mode: str) -> str:
return ImageMode.getmode(mode).basemode 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 Gets the storage type mode. Given a mode, this function returns a
single-layer mode suitable for storing individual bands. single-layer mode suitable for storing individual bands.
@ -279,7 +280,7 @@ def getmodetype(mode):
return ImageMode.getmode(mode).basetype 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 Gets a list of individual band names. Given a mode, this function returns
a tuple containing the names of individual bands (use a tuple containing the names of individual bands (use
@ -311,7 +312,7 @@ def getmodebands(mode: str) -> int:
_initialized = 0 _initialized = 0
def preinit(): def preinit() -> None:
""" """
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.
@ -437,7 +438,7 @@ def _getencoder(mode, encoder_name, args, extra=()):
class _E: class _E:
def __init__(self, scale, offset): def __init__(self, scale, offset) -> None:
self.scale = scale self.scale = scale
self.offset = offset self.offset = offset
@ -508,22 +509,22 @@ class Image:
self._exif = None self._exif = None
@property @property
def width(self): def width(self) -> int:
return self.size[0] return self.size[0]
@property @property
def height(self): def height(self) -> int:
return self.size[1] return self.size[1]
@property @property
def size(self): def size(self) -> tuple[int, int]:
return self._size return self._size
@property @property
def mode(self): def mode(self):
return self._mode return self._mode
def _new(self, im): def _new(self, im) -> Image:
new = Image() new = Image()
new.im = im new.im = im
new._mode = im.mode new._mode = im.mode
@ -556,7 +557,7 @@ class Image:
self._close_fp() self._close_fp()
self.fp = None self.fp = None
def close(self): def close(self) -> None:
""" """
Closes the file pointer, if possible. Closes the file pointer, if possible.
@ -589,7 +590,7 @@ class Image:
self.pyaccess = None self.pyaccess = None
self.readonly = 0 self.readonly = 0
def _ensure_mutable(self): def _ensure_mutable(self) -> None:
if self.readonly: if self.readonly:
self._copy() self._copy()
else: else:
@ -629,7 +630,7 @@ class Image:
and self.tobytes() == other.tobytes() and self.tobytes() == other.tobytes()
) )
def __repr__(self): def __repr__(self) -> str:
return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % (
self.__class__.__module__, self.__class__.__module__,
self.__class__.__name__, self.__class__.__name__,
@ -639,7 +640,7 @@ class Image:
id(self), id(self),
) )
def _repr_pretty_(self, p, cycle): def _repr_pretty_(self, p, cycle) -> None:
"""IPython plain text display support""" """IPython plain text display support"""
# Same as __repr__ but without unpredictable id(self), # Same as __repr__ but without unpredictable id(self),
@ -711,7 +712,7 @@ class Image:
im_data = self.tobytes() # load image first im_data = self.tobytes() # load image first
return [self.info, self.mode, self.size, self.getpalette(), im_data] return [self.info, self.mode, self.size, self.getpalette(), im_data]
def __setstate__(self, state): def __setstate__(self, state) -> None:
Image.__init__(self) Image.__init__(self)
info, mode, size, palette, data = state info, mode, size, palette, data = state
self.info = info self.info = info
@ -774,7 +775,7 @@ class Image:
return b"".join(output) return b"".join(output)
def tobitmap(self, name="image"): def tobitmap(self, name: str = "image") -> bytes:
""" """
Returns the image converted to an X11 bitmap. Returns the image converted to an X11 bitmap.
@ -886,7 +887,12 @@ class Image:
pass pass
def convert( 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: ) -> Image:
""" """
Returns a converted copy of this image. For the "P" mode, this Returns a converted copy of this image. For the "P" mode, this
@ -1117,12 +1123,12 @@ class Image:
def quantize( def quantize(
self, self,
colors=256, colors: int = 256,
method=None, method: Quantize | None = None,
kmeans=0, kmeans: int = 0,
palette=None, palette=None,
dither=Dither.FLOYDSTEINBERG, dither: Dither = Dither.FLOYDSTEINBERG,
): ) -> Image:
""" """
Convert the image to 'P' mode with the specified number Convert the image to 'P' mode with the specified number
of colors. of colors.
@ -1210,7 +1216,7 @@ class Image:
__copy__ = copy __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 Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel 4-tuple defining the left, upper, right, and lower pixel
@ -1341,7 +1347,7 @@ class Image:
self.load() self.load()
return self.im.getbbox(alpha_only) 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. Returns a list of colors used in this image.
@ -1364,7 +1370,7 @@ class Image:
return out return out
return self.im.getcolors(maxcolors) 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 Returns the contents of this image as a sequence object
containing pixel values. The sequence object is flattened, so containing pixel values. The sequence object is flattened, so
@ -1387,7 +1393,7 @@ class Image:
return self.im.getband(band) return self.im.getband(band)
return self.im # could be abused 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 Gets the minimum and maximum pixel values for each band in
the image. the image.
@ -1468,7 +1474,7 @@ class Image:
return self._exif return self._exif
def _reload_exif(self): def _reload_exif(self) -> None:
if self._exif is None or not self._exif._loaded: if self._exif is None or not self._exif._loaded:
return return
self._exif._loaded = False self._exif._loaded = False
@ -1605,7 +1611,7 @@ class Image:
return self.pyaccess.getpixel(xy) return self.pyaccess.getpixel(xy)
return self.im.getpixel(tuple(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 Get projection to x and y axes
@ -1617,7 +1623,7 @@ class Image:
x, y = self.im.getprojection() x, y = self.im.getprojection()
return list(x), list(y) 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 Returns a histogram for the image. The histogram is returned as a
list of pixel counts, one for each pixel value in the source list of pixel counts, one for each pixel value in the source
@ -2463,7 +2469,7 @@ class Image:
if open_fp: if open_fp:
fp.close() 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 Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an beyond the end of the sequence, the method raises an
@ -2485,7 +2491,7 @@ class Image:
msg = "no more images in file" msg = "no more images in file"
raise EOFError(msg) 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. Displays this image. This method is mainly intended for debugging purposes.
@ -2526,7 +2532,7 @@ class Image:
return (self.copy(),) return (self.copy(),)
return tuple(map(self._new, self.im.split())) 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. Returns an image containing a single channel of the source image.
@ -2601,13 +2607,13 @@ class Image:
provided_size = tuple(map(math.floor, size)) provided_size = tuple(map(math.floor, size))
def preserve_aspect_ratio(): def preserve_aspect_ratio() -> tuple[int, int] | None:
def round_aspect(number, key): def round_aspect(number, key):
return max(min(math.floor(number), math.ceil(number), key=key), 1) return max(min(math.floor(number), math.ceil(number), key=key), 1)
x, y = provided_size x, y = provided_size
if x >= self.width and y >= self.height: if x >= self.width and y >= self.height:
return return None
aspect = self.width / self.height aspect = self.width / self.height
if x / y >= aspect: if x / y >= aspect:
@ -2927,7 +2933,9 @@ def _check_size(size):
return True 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. Creates a new image with the given mode and size.
@ -3193,7 +3201,7 @@ _fromarray_typemap = {
} }
def _decompression_bomb_check(size): def _decompression_bomb_check(size: tuple[int, int]) -> None:
if MAX_IMAGE_PIXELS is None: if MAX_IMAGE_PIXELS is None:
return return
@ -3335,7 +3343,7 @@ def open(fp, mode="r", formats=None) -> Image:
# Image processing. # Image processing.
def alpha_composite(im1, im2): def alpha_composite(im1: Image, im2: Image) -> Image:
""" """
Alpha composite im2 over im1. Alpha composite im2 over im1.
@ -3350,7 +3358,7 @@ def alpha_composite(im1, im2):
return im1._new(core.alpha_composite(im1.im, im2.im)) 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 Creates a new image by interpolating between two input images, using
a constant alpha:: a constant alpha::
@ -3373,7 +3381,7 @@ def blend(im1, im2, alpha):
return im1._new(core.blend(im1.im, im2.im, 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. 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 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 Registers an image function to save all the frames
of a multiframe format. This function should not be 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. # Simple display support.
def _show(image, **options): def _show(image, **options) -> None:
from . import ImageShow from . import ImageShow
ImageShow.show(image, **options) ImageShow.show(image, **options)
@ -3613,7 +3621,7 @@ def radial_gradient(mode):
# Resources # Resources
def _apply_env_variables(env=None): def _apply_env_variables(env=None) -> None:
if env is None: if env is None:
env = os.environ env = os.environ
@ -3928,13 +3936,13 @@ class Exif(_ExifBase):
} }
return ifd return ifd
def hide_offsets(self): def hide_offsets(self) -> None:
for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo):
if tag in self: if tag in self:
self._hidden_data[tag] = self[tag] self._hidden_data[tag] = self[tag]
del self[tag] del self[tag]
def __str__(self): def __str__(self) -> str:
if self._info is not None: if self._info is not None:
# Load all keys into self._data # Load all keys into self._data
for tag in self._info: for tag in self._info:
@ -3942,7 +3950,7 @@ class Exif(_ExifBase):
return str(self._data) return str(self._data)
def __len__(self): def __len__(self) -> int:
keys = set(self._data) keys = set(self._data)
if self._info is not None: if self._info is not None:
keys.update(self._info) keys.update(self._info)
@ -3954,10 +3962,10 @@ class Exif(_ExifBase):
del self._info[tag] del self._info[tag]
return self._data[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) 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: if self._info is not None and tag in self._info:
del self._info[tag] del self._info[tag]
self._data[tag] = value self._data[tag] = value

View File

@ -18,9 +18,10 @@ from __future__ import annotations
import builtins import builtins
from types import CodeType from types import CodeType
from typing import Any from typing import Any, Callable
from . import Image, _imagingmath from . import Image, _imagingmath
from ._deprecate import deprecate
class _Operand: class _Operand:
@ -235,9 +236,55 @@ ops = {
} }
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 expression: A string containing a Python-style expression.
:param options: Values to add to the evaluation context. You :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 # build execution namespace
args: dict[str, Any] = ops.copy() 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): if "__" in k or hasattr(builtins, k):
msg = f"'{k}' not allowed" msg = f"'{k}' not allowed"
raise ValueError(msg) raise ValueError(msg)
args.update(_dict) args.update(options)
args.update(kw) args.update(kw)
for k, v in args.items(): for k, v in args.items():
if hasattr(v, "im"): if hasattr(v, "im"):
@ -279,3 +326,32 @@ def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any:
return out.im return out.im
except AttributeError: except AttributeError:
return out 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)