From ff6ca4159a0f9547472eacbe42596d14bc95725b Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Tue, 24 Mar 2020 19:19:46 +1100
Subject: [PATCH 1/4] Prevent masking Image reduce method

---
 Tests/test_file_jpeg2k.py    |  9 +++++++++
 Tests/test_image_reduce.py   | 11 +++++++++++
 src/PIL/Image.py             |  6 +++++-
 src/PIL/Jpeg2KImagePlugin.py | 18 +++++++++++++-----
 4 files changed, 38 insertions(+), 6 deletions(-)

diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index e37b46a41d3..0a80d9dee92 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -127,10 +127,19 @@ def test_prog_res_rt():
 
 def test_reduce():
     with Image.open("Tests/images/test-card-lossless.jp2") as im:
+        assert callable(im.reduce)
+
         im.reduce = 2
+        assert im.reduce == 2
+
         im.load()
         assert im.size == (160, 120)
 
+        try:
+            im.thumbnail((40, 40))
+        except ValueError as e:
+            assert str(e) == "box can't exceed original image size"
+
 
 def test_layers_type(tmp_path):
     outfile = str(tmp_path / "temp_layers.jp2")
diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py
index 658a0f5134e..729645a0b2b 100644
--- a/Tests/test_image_reduce.py
+++ b/Tests/test_image_reduce.py
@@ -3,6 +3,9 @@
 
 from .helper import convert_to_comparable
 
+codecs = dir(Image.core)
+
+
 # There are several internal implementations
 remarkable_factors = [
     # special implementations
@@ -247,3 +250,11 @@ def test_mode_F():
     for factor in remarkable_factors:
         compare_reduce_with_reference(im, factor, 0, 0)
         compare_reduce_with_box(im, factor)
+
+
+@pytest.mark.skipif(
+    "jpeg2k_decoder" not in codecs, reason="JPEG 2000 support not available"
+)
+def test_jpeg2k():
+    with Image.open("Tests/images/test-card-lossless.jp2") as im:
+        assert im.reduce(2).size == (320, 240)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index b1e8ad3ea06..f296ff86bbf 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1866,7 +1866,11 @@ def resize(self, size, resample=BICUBIC, box=None, reducing_gap=None):
             factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1
             if factor_x > 1 or factor_y > 1:
                 reduce_box = self._get_safe_box(size, resample, box)
-                self = self.reduce((factor_x, factor_y), box=reduce_box)
+                factor = (factor_x, factor_y)
+                if callable(self.reduce):
+                    self = self.reduce(factor, box=reduce_box)
+                else:
+                    self = Image.reduce(self, factor, box=reduce_box)
                 box = (
                     (box[0] - reduce_box[0]) / factor_x,
                     (box[1] - reduce_box[1]) / factor_y,
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 2c51d36780a..19499959f14 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -176,7 +176,7 @@ def _open(self):
         if self.size is None or self.mode is None:
             raise SyntaxError("unable to determine size/mode")
 
-        self.reduce = 0
+        self._reduce = 0
         self.layers = 0
 
         fd = -1
@@ -200,13 +200,21 @@ def _open(self):
                 "jpeg2k",
                 (0, 0) + self.size,
                 0,
-                (self.codec, self.reduce, self.layers, fd, length),
+                (self.codec, self._reduce, self.layers, fd, length),
             )
         ]
 
+    @property
+    def reduce(self):
+        return self._reduce or super().reduce
+
+    @reduce.setter
+    def reduce(self, value):
+        self._reduce = value
+
     def load(self):
-        if self.reduce:
-            power = 1 << self.reduce
+        if self._reduce:
+            power = 1 << self._reduce
             adjust = power >> 1
             self._size = (
                 int((self.size[0] + adjust) / power),
@@ -216,7 +224,7 @@ def load(self):
         if self.tile:
             # Update the reduce and layers settings
             t = self.tile[0]
-            t3 = (t[3][0], self.reduce, self.layers, t[3][3], t[3][4])
+            t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
             self.tile = [(t[0], (0, 0) + self.size, t[2], t3)]
 
         return ImageFile.ImageFile.load(self)

From 4f9118bdbd83b32723be1a8d364806686f1d3108 Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Fri, 20 Mar 2020 17:49:36 +1100
Subject: [PATCH 2/4] Added comment [ci skip]

---
 src/PIL/Jpeg2KImagePlugin.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 19499959f14..2dab7808f2f 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -206,6 +206,9 @@ def _open(self):
 
     @property
     def reduce(self):
+        # https://github.com/python-pillow/Pillow/issues/4343 found that the
+        # new Image 'reduce' method was shadowed by this plugin's 'reduce'
+        # property. This attempts to allow for both scenarios
         return self._reduce or super().reduce
 
     @reduce.setter

From 6d8f2f95db026c61aeb6298b242c16d573cdf3bb Mon Sep 17 00:00:00 2001
From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Date: Fri, 20 Mar 2020 18:14:08 +1100
Subject: [PATCH 3/4] Do not reduce size if tile already loaded

Co-Authored-By: Alexander Karpinsky <homm86@gmail.com>
---
 src/PIL/Jpeg2KImagePlugin.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 2dab7808f2f..0b0d433db41 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -216,7 +216,7 @@ def reduce(self, value):
         self._reduce = value
 
     def load(self):
-        if self._reduce:
+        if self.tile and self._reduce:
             power = 1 << self._reduce
             adjust = power >> 1
             self._size = (
@@ -224,7 +224,6 @@ def load(self):
                 int((self.size[1] + adjust) / power),
             )
 
-        if self.tile:
             # Update the reduce and layers settings
             t = self.tile[0]
             t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])

From d00b9296514262acdf88fe2bcde7d2ed9a091afa Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Sun, 29 Mar 2020 16:29:34 +1100
Subject: [PATCH 4/4] Updated test

---
 Tests/test_file_jpeg2k.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 0a80d9dee92..72bc7df672f 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -135,10 +135,8 @@ def test_reduce():
         im.load()
         assert im.size == (160, 120)
 
-        try:
-            im.thumbnail((40, 40))
-        except ValueError as e:
-            assert str(e) == "box can't exceed original image size"
+        im.thumbnail((40, 40))
+        assert im.size == (40, 30)
 
 
 def test_layers_type(tmp_path):
