diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b5ea6d0a0..677a14981 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -209,6 +209,49 @@ def test_layers(): assert_image_similar(im, test_card, 0.4) +@pytest.mark.parametrize( + "name, args, offset, data", + ( + ("foo.j2k", {}, 0, b"\xff\x4f"), + ("foo.jp2", {}, 4, b"jP"), + (None, {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ), +) +def test_no_jp2(name, args, offset, data): + out = BytesIO() + if name: + out.name = name + test_card.save(out, "JPEG2000", **args) + out.seek(offset) + assert out.read(2) == data + + +def test_mct(): + # Three component + for val in (0, 1): + out = BytesIO() + test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[59] == val + with Image.open(out) as im: + assert_image_similar(im, test_card, 1.0e-3) + + # Single component should have MCT disabled + for val in (0, 1): + out = BytesIO() + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + jp2.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[53] == 0 + with Image.open(out) as im: + assert_image_similar(im, jp2, 1.0e-3) + + def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 4ea36013f..15d80ab24 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -504,9 +504,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: and must be greater than the code-block size. **irreversible** - If ``True``, use the lossy Irreversible Color Transformation - followed by DWT 9-7. Defaults to ``False``, which means to use the - Reversible Color Transformation with DWT 5-3. + If ``True``, use the lossy discrete waveform transformation DWT 9-7. + Defaults to ``False``, which uses the lossless DWT 5-3. + +**mct** + If ``1`` then enable multiple component transformation when encoding, + otherwise use ``0`` for no component transformation (default). If MCT is + enabled and ``irreversible`` is ``True`` then the Irreversible Color + Transformation will be applied, otherwise encoding will use the + Reversible Color Transformation. MCT works best with a ``mode`` of + ``RGB`` and is only applicable when the image data has 3 components. + + .. versionadded:: 9.1.0 **progression** Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``, @@ -526,6 +535,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: for compliant 4K files, *at least one* of the dimensions must match 4096 x 2160. +**no_jp2** + If ``True`` then don't wrap the raw codestream in the JP2 file format when + saving, otherwise the extension of the filename will be used to determine + the format (default). + + .. versionadded:: 9.1.0 + .. note:: To enable JPEG 2000 support, you need to build and install the OpenJPEG diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 5a0bdba3e..00900e356 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -146,12 +146,24 @@ At present, the information within each block is merely returned as a dictionary "data" entry. This will allow more useful information to be added in the future without breaking backwards compatibility. -Added rawmode argument to Image.getpalette() -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Added mct and no_jp2 options for saving JPEG 2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the palette. -A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None`` -can be used to return data in the current mode of the palette. +The :py:meth:`PIL.Image.Image.save` method now supports the following options for +JPEG 2000: + +**mct** + If ``1`` then enable multiple component transformation when encoding, + otherwise use ``0`` for no component transformation (default). If MCT is + enabled and ``irreversible`` is ``True`` then the Irreversible Color + Transformation will be applied, otherwise encoding will use the + Reversible Color Transformation. MCT works best with a ``mode`` of + ``RGB`` and is only applicable when the image data has 3 components. + +**no_jp2** + If ``True`` then don't wrap the raw codestream in the JP2 file format when + saving, otherwise the extension of the filename will be used to determine + the format (default). Added PyEncoder ^^^^^^^^^^^^^^^ diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 4f4ee8f55..fb5d70cee 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -290,14 +290,14 @@ def _accept(prefix): def _save(im, fp, filename): - if filename.endswith(".j2k"): + # Get the keyword arguments + info = im.encoderinfo + + if filename.endswith(".j2k") or info.get("no_jp2", False): kind = "j2k" else: kind = "jp2" - # Get the keyword arguments - info = im.encoderinfo - offset = info.get("offset", None) tile_offset = info.get("tile_offset", None) tile_size = info.get("tile_size", None) @@ -320,6 +320,7 @@ def _save(im, fp, filename): irreversible = info.get("irreversible", False) progression = info.get("progression", "LRCP") cinema_mode = info.get("cinema_mode", "no") + mct = info.get("mct", 0) fd = -1 if hasattr(fp, "fileno"): @@ -340,6 +341,7 @@ def _save(im, fp, filename): irreversible, progression, cinema_mode, + mct, fd, ) diff --git a/src/encode.c b/src/encode.c index a52d48f62..72c7f64d0 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1187,11 +1187,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { OPJ_PROG_ORDER prog_order; char *cinema_mode = "no"; OPJ_CINEMA_MODE cine_mode; + char mct = 0; Py_ssize_t fd = -1; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssn", + "ss|OOOsOnOOOssbn", &mode, &format, &offset, @@ -1205,6 +1206,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &irreversible, &progression, &cinema_mode, + &mct, &fd)) { return NULL; } @@ -1302,6 +1304,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->irreversible = PyObject_IsTrue(irreversible); context->progression = prog_order; context->cinema_mode = cine_mode; + context->mct = mct; return (PyObject *)encoder; } diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index f749ecfb2..d030b0c43 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -82,6 +82,9 @@ typedef struct { /* Compression style */ int irreversible; + /* Set multiple component transformation */ + char mct; + /* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */ OPJ_PROG_ORDER progression; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 86cd7d5af..fe5511ba5 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -435,6 +435,9 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } params.irreversible = context->irreversible; + if (components == 3) { + params.tcp_mct = context->mct; + } params.prog_order = context->progression;