From 4e12ccc63e40a9b567af3b2e1ac821f5157cddc6 Mon Sep 17 00:00:00 2001 From: Ben Rudiak-Gould Date: Sat, 30 Apr 2022 22:58:44 -0700 Subject: [PATCH 1/5] Support more affine expression forms in Image.point In modes I and F, Image.point only supported affine expressions of the forms (lambda x:) x * a, x + a, and x * a + b. Expressions like 1 - x had to be written x * -1 + 1. This rewrite, though still limited to affine transformations, supports far more expression forms, including 1 - x, (2 * x + 1) / 3, etc. --- Tests/test_image_point.py | 12 ++++++-- src/PIL/Image.py | 61 +++++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 428ad116b3d..2a4218bf854 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -18,10 +18,18 @@ def test_sanity(): im.point(lambda x: x * 1) im.point(lambda x: x + 1) im.point(lambda x: x * 1 + 1) + im.point(lambda x: 0.1 + 0.2 * x) + im.point(lambda x: -x) + im.point(lambda x: x - 0.5) + im.point(lambda x: 1 - x / 2) + im.point(lambda x: (2 + x) / 3) + im.point(lambda x: 0.5) with pytest.raises(TypeError): - im.point(lambda x: x - 1) + im.point(lambda x: x * x) with pytest.raises(TypeError): - im.point(lambda x: x / 1) + im.point(lambda x: 1 / x) + with pytest.raises(TypeError): + im.point(lambda x: x // 2) def test_16bit_lut(): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 99c7ba0d1a1..e3a1eac70cb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -431,45 +431,44 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -def coerce_e(value): - return value if isinstance(value, _E) else _E(value) +# _Affine(m, b) represents the polynomial m x + b +class _Affine: + def __init__(self, m, b): + self.m = m + self.b = b - -class _E: - def __init__(self, data): - self.data = data + def __neg__(self): + return _Affine(-self.m, -self.b) def __add__(self, other): - return _E((self.data, "__add__", coerce_e(other).data)) + if isinstance(other, _Affine): + return _Affine(self.m + other.m, self.b + other.b) + return _Affine(self.m, self.b + other) + + __radd__ = __add__ + + def __sub__(self, other): + return self + -other + + def __rsub__(self, other): + return other + -self def __mul__(self, other): - return _E((self.data, "__mul__", coerce_e(other).data)) + if isinstance(other, _Affine): + return NotImplemented + return _Affine(self.m * other, self.b * other) + + __rmul__ = __mul__ + + def __truediv__(self, other): + if isinstance(other, _Affine): + return NotImplemented + return _Affine(self.m / other, self.b / other) def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if a is stub and b == "__mul__" and isinstance(c, numbers.Number): - return c, 0.0 - if a is stub and b == "__add__" and isinstance(c, numbers.Number): - return 1.0, c - except TypeError: - pass - try: - ((a, b, c), d, e) = data # full syntax - if ( - a is stub - and b == "__mul__" - and isinstance(c, numbers.Number) - and d == "__add__" - and isinstance(e, numbers.Number) - ): - return c, e - except TypeError: - pass - raise ValueError("illegal expression") + a = expr(_Affine(1.0, 0.0)) + return (a.m, a.b) if isinstance(a, _Affine) else (0.0, a) # -------------------------------------------------------------------- From 46802d5def59b6694e5243e428b6419dc8a5ab43 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 May 2022 09:01:23 +1000 Subject: [PATCH 2/5] Removed unused import and restored existing checks (#1) * Removed unused import * Restored existing checks * Restored coerce_e, _E and data property * Deprecated coerce_e Co-authored-by: Andrew Murray --- Tests/test_image_point.py | 9 +++++++++ src/PIL/Image.py | 35 +++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 2a4218bf854..140b7a3c916 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,5 +1,7 @@ import pytest +from PIL import Image + from .helper import assert_image_equal, hopper @@ -17,6 +19,7 @@ def test_sanity(): im.point(list(range(256))) im.point(lambda x: x * 1) im.point(lambda x: x + 1) + im.point(lambda x: x - 1) im.point(lambda x: x * 1 + 1) im.point(lambda x: 0.1 + 0.2 * x) im.point(lambda x: -x) @@ -24,6 +27,7 @@ def test_sanity(): im.point(lambda x: 1 - x / 2) im.point(lambda x: (2 + x) / 3) im.point(lambda x: 0.5) + im.point(lambda x: x / 1) with pytest.raises(TypeError): im.point(lambda x: x * x) with pytest.raises(TypeError): @@ -55,3 +59,8 @@ def test_f_mode(): im = hopper("F") with pytest.raises(ValueError): im.point(None) + + +def test_coerce_e_deprecation(): + with pytest.warns(DeprecationWarning): + assert Image.coerce_e(2).data == 2 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e3a1eac70cb..114f4adb3eb 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -29,7 +29,6 @@ import io import logging import math -import numbers import os import re import struct @@ -431,19 +430,23 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -# _Affine(m, b) represents the polynomial m x + b -class _Affine: - def __init__(self, m, b): - self.m = m - self.b = b +def coerce_e(value): + deprecate("coerce_e", 10) + return value if isinstance(value, _E) else _E(1, value) + + +class _E: + def __init__(self, scale, data): + self.scale = scale + self.data = data def __neg__(self): - return _Affine(-self.m, -self.b) + return _E(-self.scale, -self.data) def __add__(self, other): - if isinstance(other, _Affine): - return _Affine(self.m + other.m, self.b + other.b) - return _Affine(self.m, self.b + other) + if isinstance(other, _E): + return _E(self.scale + other.scale, self.data + other.data) + return _E(self.scale, self.data + other) __radd__ = __add__ @@ -454,21 +457,21 @@ def __rsub__(self, other): return other + -self def __mul__(self, other): - if isinstance(other, _Affine): + if isinstance(other, _E): return NotImplemented - return _Affine(self.m * other, self.b * other) + return _E(self.scale * other, self.data * other) __rmul__ = __mul__ def __truediv__(self, other): - if isinstance(other, _Affine): + if isinstance(other, _E): return NotImplemented - return _Affine(self.m / other, self.b / other) + return _E(self.scale / other, self.data / other) def _getscaleoffset(expr): - a = expr(_Affine(1.0, 0.0)) - return (a.m, a.b) if isinstance(a, _Affine) else (0.0, a) + a = expr(_E(1, 0)) + return (a.scale, a.data) if isinstance(a, _E) else (0, a) # -------------------------------------------------------------------- From 88f46f3c998e15ebec8b93df32316b95f7639432 Mon Sep 17 00:00:00 2001 From: Ben Rudiak-Gould Date: Tue, 3 May 2022 13:42:04 -0700 Subject: [PATCH 3/5] Add a comment --- src/PIL/Image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 114f4adb3eb..09214e2f93b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -435,6 +435,9 @@ def coerce_e(value): return value if isinstance(value, _E) else _E(1, value) +# _E(scale, offset) represents the affine transformation scale * x + offset. +# The "data" field is named for compatibility with the old implementation, +# and should be renamed once coerce_e is removed. class _E: def __init__(self, scale, data): self.scale = scale From 48f763a3785933aaed8d52a983ddae287e8f235c Mon Sep 17 00:00:00 2001 From: Ben Rudiak-Gould Date: Tue, 3 May 2022 13:53:50 -0700 Subject: [PATCH 4/5] Manually merge radarhere's additional tests --- Tests/test_image_point.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 140b7a3c916..157ecb120f0 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -28,8 +28,11 @@ def test_sanity(): im.point(lambda x: (2 + x) / 3) im.point(lambda x: 0.5) im.point(lambda x: x / 1) + im.point(lambda x: x + x) with pytest.raises(TypeError): im.point(lambda x: x * x) + with pytest.raises(TypeError): + im.point(lambda x: x / x) with pytest.raises(TypeError): im.point(lambda x: 1 / x) with pytest.raises(TypeError): From c7f5b4c2daa8882b95564127f174d94575c420d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 17 May 2022 17:31:18 +1000 Subject: [PATCH 5/5] Documented deprecation --- docs/deprecations.rst | 8 ++++++++ docs/releasenotes/9.2.0.rst | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index ad030acd071..8c5b8a748d7 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -170,6 +170,14 @@ in Pillow 10 (2023-07-01). Upgrade to `PyQt6 `_ or `PySide6 `_ instead. +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +This undocumented method has been deprecated and will be removed in Pillow 10 +(2023-07-01). + Removed features ---------------- diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index c38944b10e9..db051d1881f 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -31,6 +31,14 @@ FreeTypeFont.getmask2 fill parameter The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been deprecated and will be removed in Pillow 10 (2023-07-01). +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +This undocumented method has been deprecated and will be removed in Pillow 10 +(2023-07-01). + API Changes ===========