Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple component transformation to JPEG2000 #5500

Merged
merged 12 commits into from
Apr 1, 2022
43 changes: 43 additions & 0 deletions Tests/test_file_jpeg2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 19 additions & 3 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -502,9 +502,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"``,
Expand All @@ -524,6 +533,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
Expand Down
24 changes: 18 additions & 6 deletions docs/releasenotes/9.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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.
Added mct and no_jp2 options for saving JPEG 2000
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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
^^^^^^^^^^^^^^^
Expand Down
10 changes: 6 additions & 4 deletions src/PIL/Jpeg2KImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"):
Expand All @@ -340,6 +341,7 @@ def _save(im, fp, filename):
irreversible,
progression,
cinema_mode,
mct,
fd,
)

Expand Down
5 changes: 4 additions & 1 deletion src/encode.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1205,6 +1206,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
&irreversible,
&progression,
&cinema_mode,
&mct,
&fd)) {
return NULL;
}
Expand Down Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/libImaging/Jpeg2K.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions src/libImaging/Jpeg2KEncode.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down