Repository: python-pillow/Pillow

Issue:
Empty JPEG2000 image after upgrading from 10.2.0 to 10.3.0
### What did you do?

I want to load a JPEG2000 image from raw data into *Pillow* for further processing. After upgrading from 10.2.0 to 10.3.0, this broke for a specific image.

### What did you expect to happen?

The image is being loaded as before.

### What actually happened?

The image is not being loaded correctly and just empty.

### What are your OS, Python and Pillow versions?

* OS: OpenSUSE Leap 15.4, Ubuntu 22.04 (through GitHub Actions)
* Python: 3.9.18, 3.12.2 (through GitHub Actions)
* Pillow: 10.3.0

```text
--------------------------------------------------------------------
Pillow 10.3.0
Python 3.9.18 (main, Sep 06 2023, 07:49:32) [GCC]
--------------------------------------------------------------------
Python executable is /home/stefan/temp/venv/bin/python3
Environment Python files loaded from /home/stefan/temp/venv
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from /home/stefan/temp/venv/lib64/python3.9/site-packages/PIL
Binary Pillow modules loaded from /home/stefan/temp/venv/lib64/python3.9/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 10.3.0
*** TKINTER support not installed
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.3.2
--- WEBP Transparency support ok
--- WEBPMUX support ok
--- WEBP Animation support ok
--- JPEG support ok, compiled for libjpeg-turbo 3.0.2
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.2
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.11
--- LIBTIFF support ok, loaded 4.6.0
--- RAQM (Bidirectional Text) support ok, loaded 0.10.1, fribidi 1.0.10, harfbuzz 8.4.0
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
```

```python
from PIL import Image, ImageFile
from io import BytesIO

ImageFile.LOAD_TRUNCATED_IMAGES = True

with open("raw.txt", mode="rb") as fd:
    data = fd.read()
image = Image.open(BytesIO(data), formats=("JPEG2000",))
print(image)
image.load()
image.save(f"{Image.__version__}.png")
```

Corresponding raw data: [raw.txt](https://github.com/python-pillow/Pillow/files/14875677/raw.txt)

With version 10.2.0:

![10 2 0](https://github.com/python-pillow/Pillow/assets/96178532/2505373f-ba1e-460c-985c-e069c000ed9a)

With version 10.3.0:

![10 3 0](https://github.com/python-pillow/Pillow/assets/96178532/b3b568d0-aba6-454d-a176-7d6033992c2e)


Corresponding issue where this was discovered: https://github.com/py-pdf/pypdf/issues/2568


Discussion:
Investigating, the change didn't occur in Pillow's code itself, but rather as a result of upgrading from OpenJPEG 2.5.0 to 2.5.2.
I've created https://github.com/python-pillow/Pillow/pull/7947 to resolve this. With that change,
```python
from PIL import Image, ImageFile
from io import BytesIO

ImageFile.LOAD_TRUNCATED_IMAGES = True

with open("raw.txt", mode="rb") as fd:
    data = fd.read()
image = Image.open(BytesIO(data), formats=("JPEG2000",))
image.save(f"{Image.__version__}.jpg")
```
creates
![10 4 0 dev0](https://github.com/python-pillow/Pillow/assets/3112309/ec1c8aee-d836-4951-8395-19200e85d5a2)

This isn't the same as 10.2.0, but I think you will agree it is correct when compared to the original image data.

Resolving patch:
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -176,6 +176,10 @@ def _parse_jp2_header(fp):
                 mode = "RGB"
             elif nc == 4:
                 mode = "RGBA"
+        elif tbox == b"colr" and nc == 4:
+            meth, _, _, enumcs = header.read_fields(">BBBI")
+            if meth == 1 and enumcs == 12:
+                mode = "CMYK"
         elif tbox == b"pclr" and mode in ("L", "LA"):
             ne, npc = header.read_fields(">HB")
             bitdepths = header.read_fields(">" + ("B" * npc))


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
@@ -289,6 +289,16 @@ def test_rgba(ext: str) -> None:
         assert im.mode == "RGBA"
 
 
+@pytest.mark.skipif(
+    not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
+)
+@skip_unless_feature_version("jpg_2000", "2.5.1")
+def test_cmyk() -> None:
+    with Image.open(f"{EXTRA_DIR}/issue205.jp2") as im:
+        assert im.mode == "CMYK"
+        assert im.getpixel((0, 0)) == (185, 134, 0, 0)
+
+
 @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
 def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
     with Image.open("Tests/images/16bit.cropped" + ext) as im: