Repository: python-pillow/Pillow

Problem statement:
JPEG 2000 file cannot be Image.reduce() 'd
JPEG 2000 file cannot be Image.reduce() 'd

... because img.reduce becomes a integer 0 during loading somehow.

Can be repeated with

[this image](https://drive.google.com/open?id=1UbSkNdOuUCt50TTu7Zc4PnuP6f-5RkKK)

```
$ python -c 'import PIL;from PIL import Image; im = Image.open("pillow_bug.jp2"); print(PIL.__version__);print(im.reduce)'
7.1.0.dev0
0
```


Hints:
Hi. Thanks for reporting this. The bug you've found applies to Jpeg2000 images. #4251 added an Image `reduce` method which is here overlapping with Jpeg2KImagePlugin's `reduce` property. I've created PR #4344 to resolve the situation.

In the meantime, you can workaround this by simply copying the image first -

```python
from PIL import Image
im = Image.open("pillow_bug.jp2")
im.copy().reduce(2)
```
Unfortunately, this breaks a very common use case: 
```python
Image.open("pillow_bug.jp2").thumbnail((100, 100))
```
because thumbnail now uses `.reduce()` method internal.

Patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- 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
--- 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,23 +200,33 @@ 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):
+        # 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
+    def reduce(self, value):
+        self._reduce = value
+
     def load(self):
-        if self.reduce:
-            power = 1 << self.reduce
+        if self.tile and self._reduce:
+            power = 1 << self._reduce
             adjust = power >> 1
             self._size = (
                 int((self.size[0] + adjust) / power),
                 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])
+            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)


Test patch:
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -127,10 +127,17 @@ 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)
 
+        im.thumbnail((40, 40))
+        assert im.size == (40, 30)
+
 
 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
--- 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)