From fcf5b7ef8319730df1de5df76cdebe427ae4a4c3 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 11:33:15 +1100 Subject: [PATCH 01/19] Fixed merge conflicts during recent pull --- src/encode.c | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/encode.c b/src/encode.c index 21c42d915..33a2a37a7 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,10 +1214,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; + char * comment = NULL; + int add_plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbn", + "ss|OOOsOnOOOssbbnzp", &mode, &format, &offset, @@ -1233,7 +1235,9 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &cinema_mode, &mct, &sgnd, - &fd)) { + &fd, + &comment, + &add_plt)) { return NULL; } @@ -1315,6 +1319,29 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } + if (comment != NULL && strlen(comment) > 0) { + /* Size is stored as as an uint16, subtract 4 bytes for the header */ + if (strlen(comment) >= 65531) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 comment is too long"); + Py_DECREF(encoder); + return NULL; + } + + context->comment = strdup(comment); + + if (context->comment == NULL) { + PyErr_SetString( + PyExc_MemoryError, + "Couldn't allocate memory for JPEG 2000 comment"); + Py_DECREF(encoder); + return NULL; + } + } else { + context->comment = NULL; + } + if (quality_layers && PySequence_Check(quality_layers)) { context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; context->quality_layers = quality_layers; @@ -1332,6 +1359,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; + context->add_plt = add_plt; return (PyObject *)encoder; } From de43bc99c873fe0c752f5e303c4a40f954b61912 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 11:37:14 +1100 Subject: [PATCH 02/19] Added support for jpeg2000 comments and PLT marker segments --- Tests/test_file_jpeg2k.py | 44 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 13 ++++++++ src/PIL/Jpeg2KImagePlugin.py | 4 +++ src/libImaging/Jpeg2K.h | 6 ++++ src/libImaging/Jpeg2KEncode.c | 17 +++++++++++ 5 files changed, 84 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b2243..f52c33402 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,5 +1,6 @@ import os import re +import struct from io import BytesIO import pytest @@ -371,3 +372,46 @@ def test_crashes(test_file): im.load() except OSError: pass + + +def test_custom_comment(): + output_stream = BytesIO() + unique_comment = "This is a unique comment, which should be found below" + test_card.save(output_stream, "JPEG2000", comment=unique_comment) + output_stream.seek(0) + data = output_stream.read() + # Lazy method to determine if the comment is in the image generated + assert(bytes(unique_comment, "utf-8") in data) + + +def test_plt_marker(): + # Search the start of the codesteam for the PLT box (id 0xFF58) + opj_version = re.search(r"(\d+\.\d+)\.\d+$", features.version_codec("jpg_2000")) + assert opj_version is not None + + if float(opj_version[1]) >= 2.4: + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + out.seek(0) + while True: + box_bytes = out.read(2) + if len(box_bytes) == 0: + # End of steam encounterd and no PLT or SOD + break + jp2_boxid = struct.unpack(">H", box_bytes)[0] + + if jp2_boxid == 0xff4f: + # No length specifier for main header + continue + elif jp2_boxid == 0xff58: + # This is the PLT box we're looking for + return + elif jp2_boxid == 0xff93: + break + # SOD box encountered and no PLT, so it wasn't found + + jp2_boxlength = struct.unpack(">H", out.read(2))[0] + out.seek(jp2_boxlength - 2, os.SEEK_CUR) + + # The PLT box wasn't found + raise ValueError diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..9128400ac 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -589,6 +589,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 +**comment** + Adds a custom comment to the file, replacing the default + "Created by OpenJPEG version" comment. + + .. versionadded:: 9.5.0 + +**add_plt** + If ``True`` then include a PLT (packet length, tile-part header) marker + segment in the produced file. + The default is to not include it. + + .. versionadded:: 9.5.0 + .. note:: To enable JPEG 2000 support, you need to build and install the OpenJPEG diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7457874c1..754010c7c 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -328,6 +328,8 @@ def _save(im, fp, filename): mct = info.get("mct", 0) signed = info.get("signed", False) fd = -1 + comment = info.get("comment", None) + add_plt = info.get("add_plt", False) if hasattr(fp, "fileno"): try: @@ -350,6 +352,8 @@ def _save(im, fp, filename): mct, signed, fd, + comment, + add_plt ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index b28a0440a..65728be5d 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -97,6 +97,12 @@ typedef struct { /* PRIVATE CONTEXT (set by decoder) */ const char *error_msg; + /* Custom comment */ + char * comment; + + /* Include PLT marker segment */ + int add_plt; + } JPEG2KENCODESTATE; /* diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index db1c5c0c9..bb280ae94 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -439,6 +439,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { params.tcp_mct = context->mct; } + if (context->comment) { + params.cp_comment = context->comment; + } + params.prog_order = context->progression; params.cp_cinema = context->cinema_mode; @@ -492,6 +496,14 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { opj_set_warning_handler(codec, j2k_warn, context); opj_setup_encoder(codec, ¶ms, image); + /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) + if (context->add_plt) { + const char * plt_option[2] = {"PLT=YES", NULL}; + opj_encoder_set_extra_options(codec, plt_option); + } +#endif + /* Start encoding */ if (!opj_start_compress(codec, image, stream)) { state->errcode = IMAGING_CODEC_BROKEN; @@ -624,7 +636,12 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { free((void *)context->error_msg); } + if (context->comment) { + free((void *)context->comment); + } + context->error_msg = NULL; + context->comment = NULL; return -1; } From 41b3ac8aed826679485913f4e1c94320408016e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Jan 2023 00:52:38 +0000 Subject: [PATCH 03/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg2k.py | 8 ++++---- src/PIL/Jpeg2KImagePlugin.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index f52c33402..6107075d5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -381,7 +381,7 @@ def test_custom_comment(): output_stream.seek(0) data = output_stream.read() # Lazy method to determine if the comment is in the image generated - assert(bytes(unique_comment, "utf-8") in data) + assert bytes(unique_comment, "utf-8") in data def test_plt_marker(): @@ -400,13 +400,13 @@ def test_plt_marker(): break jp2_boxid = struct.unpack(">H", box_bytes)[0] - if jp2_boxid == 0xff4f: + if jp2_boxid == 0xFF4F: # No length specifier for main header continue - elif jp2_boxid == 0xff58: + elif jp2_boxid == 0xFF58: # This is the PLT box we're looking for return - elif jp2_boxid == 0xff93: + elif jp2_boxid == 0xFF93: break # SOD box encountered and no PLT, so it wasn't found diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 754010c7c..001dcf39c 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -353,7 +353,7 @@ def _save(im, fp, filename): signed, fd, comment, - add_plt + add_plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) From d55563ca2519765473f87271f4f39ebd75a9150e Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 12:05:05 +1100 Subject: [PATCH 04/19] Update docs/handbook/image-file-formats.rst to fix lint Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/handbook/image-file-formats.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 9128400ac..f466ccac8 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -590,13 +590,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 **comment** - Adds a custom comment to the file, replacing the default + Adds a custom comment to the file, replacing the default "Created by OpenJPEG version" comment. .. versionadded:: 9.5.0 **add_plt** - If ``True`` then include a PLT (packet length, tile-part header) marker + If ``True`` then include a PLT (packet length, tile-part header) marker segment in the produced file. The default is to not include it. From b00bde977199968aa62c539b85ef3feb2338d080 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 22:52:41 +1100 Subject: [PATCH 05/19] Update Tests/test_file_jpeg2k.py fix spelling error Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_jpeg2k.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 6107075d5..ccc18772a 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -396,7 +396,7 @@ def test_plt_marker(): while True: box_bytes = out.read(2) if len(box_bytes) == 0: - # End of steam encounterd and no PLT or SOD + # End of steam encountered and no PLT or SOD break jp2_boxid = struct.unpack(">H", box_bytes)[0] From 79e67cb5c33e5cef781e664439bc5c7371c1c04c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 31 Jan 2023 21:42:25 +1100 Subject: [PATCH 06/19] Removed default argument --- src/PIL/Jpeg2KImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 001dcf39c..68a354e3f 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -328,7 +328,7 @@ def _save(im, fp, filename): mct = info.get("mct", 0) signed = info.get("signed", False) fd = -1 - comment = info.get("comment", None) + comment = info.get("comment") add_plt = info.get("add_plt", False) if hasattr(fp, "fileno"): From 18ad4c867ff8a53a6c40d6fdf954799b976ab7f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 21:49:12 +1100 Subject: [PATCH 07/19] Use skip_unless_feature_version --- Tests/test_file_jpeg2k.py | 50 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index ccc18772a..5669af7ab 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -12,6 +12,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, skip_unless_feature, + skip_unless_feature_version, ) EXTRA_DIR = "Tests/images/jpeg2000" @@ -384,34 +385,31 @@ def test_custom_comment(): assert bytes(unique_comment, "utf-8") in data +@skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) - opj_version = re.search(r"(\d+\.\d+)\.\d+$", features.version_codec("jpg_2000")) - assert opj_version is not None + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + out.seek(0) + while True: + box_bytes = out.read(2) + if len(box_bytes) == 0: + # End of steam encountered and no PLT or SOD + break + jp2_boxid = struct.unpack(">H", box_bytes)[0] - if float(opj_version[1]) >= 2.4: - out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) - out.seek(0) - while True: - box_bytes = out.read(2) - if len(box_bytes) == 0: - # End of steam encountered and no PLT or SOD - break - jp2_boxid = struct.unpack(">H", box_bytes)[0] + if jp2_boxid == 0xFF4F: + # No length specifier for main header + continue + elif jp2_boxid == 0xFF58: + # This is the PLT box we're looking for + return + elif jp2_boxid == 0xFF93: + break + # SOD box encountered and no PLT, so it wasn't found - if jp2_boxid == 0xFF4F: - # No length specifier for main header - continue - elif jp2_boxid == 0xFF58: - # This is the PLT box we're looking for - return - elif jp2_boxid == 0xFF93: - break - # SOD box encountered and no PLT, so it wasn't found + jp2_boxlength = struct.unpack(">H", out.read(2))[0] + out.seek(jp2_boxlength - 2, os.SEEK_CUR) - jp2_boxlength = struct.unpack(">H", out.read(2))[0] - out.seek(jp2_boxlength - 2, os.SEEK_CUR) - - # The PLT box wasn't found - raise ValueError + # The PLT box wasn't found + raise ValueError From ca97e2a3a51fcad8eab4d5466b944e512b82dadd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:31:52 +1100 Subject: [PATCH 08/19] Use _binary --- Tests/test_file_jpeg2k.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 5669af7ab..56d8a7974 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,11 +1,17 @@ import os import re -import struct from io import BytesIO import pytest -from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features +from PIL import ( + Image, + ImageFile, + Jpeg2KImagePlugin, + UnidentifiedImageError, + _binary, + features, +) from .helper import ( assert_image_equal, @@ -393,11 +399,11 @@ def test_plt_marker(): out.seek(0) while True: box_bytes = out.read(2) - if len(box_bytes) == 0: + if not box_bytes: # End of steam encountered and no PLT or SOD break - jp2_boxid = struct.unpack(">H", box_bytes)[0] + jp2_boxid = _binary.i16be(box_bytes) if jp2_boxid == 0xFF4F: # No length specifier for main header continue @@ -405,10 +411,10 @@ def test_plt_marker(): # This is the PLT box we're looking for return elif jp2_boxid == 0xFF93: - break # SOD box encountered and no PLT, so it wasn't found + break - jp2_boxlength = struct.unpack(">H", out.read(2))[0] + jp2_boxlength = _binary.i16be(out.read(2)) out.seek(jp2_boxlength - 2, os.SEEK_CUR) # The PLT box wasn't found From 4bb50b1fa7fe5ef4323b9cb0c819bfba2b608f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:24:47 +1100 Subject: [PATCH 09/19] Test comment too long --- Tests/test_file_jpeg2k.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 56d8a7974..a0fb75016 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -390,6 +390,10 @@ def test_custom_comment(): # Lazy method to determine if the comment is in the image generated assert bytes(unique_comment, "utf-8") in data + too_long_comment = " " * 65532 + with pytest.raises(ValueError): + test_card.save(output_stream, "JPEG2000", comment=too_long_comment) + @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): From 04e8a9b3e723a867f220124c26489ace5d2187e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:24:52 +1100 Subject: [PATCH 10/19] Removed unnecessary code --- src/encode.c | 6 ++---- src/libImaging/Jpeg2K.h | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/encode.c b/src/encode.c index 33a2a37a7..e8946dbae 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,7 +1214,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; - char * comment = NULL; + char *comment = NULL; int add_plt = 0; if (!PyArg_ParseTuple( @@ -1326,7 +1326,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyExc_ValueError, "JPEG 2000 comment is too long"); Py_DECREF(encoder); - return NULL; + return NULL; } context->comment = strdup(comment); @@ -1338,8 +1338,6 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { Py_DECREF(encoder); return NULL; } - } else { - context->comment = NULL; } if (quality_layers && PySequence_Check(quality_layers)) { diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 65728be5d..7bf8b4b0a 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -98,7 +98,7 @@ typedef struct { const char *error_msg; /* Custom comment */ - char * comment; + char *comment; /* Include PLT marker segment */ int add_plt; From d3923f71420c58f8545fc74fc5f41f0b415635f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Mar 2023 17:53:35 +1100 Subject: [PATCH 11/19] Use reading of comments to test saving comments --- Tests/test_file_jpeg2k.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 8261c612a..a869d74f0 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -372,6 +372,20 @@ def test_comment(): pass +def test_save_comment(): + comment = "Created by Pillow" + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + out.seek(0) + + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" + + too_long_comment = " " * 65532 + with pytest.raises(ValueError): + test_card.save(out, "JPEG2000", comment=too_long_comment) + + @pytest.mark.parametrize( "test_file", [ @@ -391,20 +405,6 @@ def test_crashes(test_file): pass -def test_custom_comment(): - output_stream = BytesIO() - unique_comment = "This is a unique comment, which should be found below" - test_card.save(output_stream, "JPEG2000", comment=unique_comment) - output_stream.seek(0) - data = output_stream.read() - # Lazy method to determine if the comment is in the image generated - assert bytes(unique_comment, "utf-8") in data - - too_long_comment = " " * 65532 - with pytest.raises(ValueError): - test_card.save(output_stream, "JPEG2000", comment=too_long_comment) - - @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) From 7c3fd254330ebcbd51fa08ef0b709b52587f5b92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Mar 2023 09:45:51 +1100 Subject: [PATCH 12/19] Allow saving bytes as comments --- Tests/test_file_jpeg2k.py | 14 +++++++------- src/PIL/Jpeg2KImagePlugin.py | 4 +++- src/encode.c | 21 +++++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a869d74f0..60be50e07 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -373,15 +373,15 @@ def test_comment(): def test_save_comment(): - comment = "Created by Pillow" - out = BytesIO() - test_card.save(out, "JPEG2000", comment=comment) - out.seek(0) + for comment in ("Created by Pillow", b"Created by Pillow"): + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + out.seek(0) - with Image.open(out) as im: - assert im.info["comment"] == b"Created by Pillow" + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" - too_long_comment = " " * 65532 + too_long_comment = " " * 65531 with pytest.raises(ValueError): test_card.save(out, "JPEG2000", comment=too_long_comment) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 4249fe714..980c299db 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -351,10 +351,12 @@ def _save(im, fp, filename): cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) signed = info.get("signed", False) - fd = -1 comment = info.get("comment") + if isinstance(comment, str): + comment = comment.encode() add_plt = info.get("add_plt", False) + fd = -1 if hasattr(fp, "fileno"): try: fd = fp.fileno() diff --git a/src/encode.c b/src/encode.c index e8946dbae..7dcb79766 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1215,11 +1215,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { int sgnd = 0; Py_ssize_t fd = -1; char *comment = NULL; + Py_ssize_t comment_size; int add_plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbnzp", + "ss|OOOsOnOOOssbbnz#p", &mode, &format, &offset, @@ -1237,6 +1238,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &sgnd, &fd, &comment, + &comment_size, &add_plt)) { return NULL; } @@ -1319,9 +1321,9 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } - if (comment != NULL && strlen(comment) > 0) { + if (comment && comment_size > 0) { /* Size is stored as as an uint16, subtract 4 bytes for the header */ - if (strlen(comment) >= 65531) { + if (comment_size >= 65531) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 comment is too long"); @@ -1329,15 +1331,14 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { return NULL; } - context->comment = strdup(comment); - - if (context->comment == NULL) { - PyErr_SetString( - PyExc_MemoryError, - "Couldn't allocate memory for JPEG 2000 comment"); + char *p = malloc(comment_size + 1); + if (!p) { Py_DECREF(encoder); - return NULL; + return ImagingError_MemoryError(); } + memcpy(p, comment, comment_size); + p[comment_size] = '\0'; + context->comment = p; } if (quality_layers && PySequence_Check(quality_layers)) { From 1fd189164c8905d2f99f0d9836ef7ea4c897b055 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Mar 2023 22:38:29 +1100 Subject: [PATCH 13/19] Renamed "add_plt" to "plt" --- Tests/test_file_jpeg2k.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 4 ++-- src/encode.c | 6 +++--- src/libImaging/Jpeg2K.h | 2 +- src/libImaging/Jpeg2KEncode.c | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 60be50e07..7b512695b 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -409,7 +409,7 @@ def test_crashes(test_file): def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + test_card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: box_bytes = out.read(2) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 980c299db..e7d91c818 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -354,7 +354,7 @@ def _save(im, fp, filename): comment = info.get("comment") if isinstance(comment, str): comment = comment.encode() - add_plt = info.get("add_plt", False) + plt = info.get("plt", False) fd = -1 if hasattr(fp, "fileno"): @@ -379,7 +379,7 @@ def _save(im, fp, filename): signed, fd, comment, - add_plt, + plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/encode.c b/src/encode.c index 7dcb79766..8aa357b6c 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1216,7 +1216,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t fd = -1; char *comment = NULL; Py_ssize_t comment_size; - int add_plt = 0; + int plt = 0; if (!PyArg_ParseTuple( args, @@ -1239,7 +1239,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &fd, &comment, &comment_size, - &add_plt)) { + &plt)) { return NULL; } @@ -1358,7 +1358,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; - context->add_plt = add_plt; + context->plt = plt; return (PyObject *)encoder; } diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 7bf8b4b0a..e8d92f7b6 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -101,7 +101,7 @@ typedef struct { char *comment; /* Include PLT marker segment */ - int add_plt; + int plt; } JPEG2KENCODESTATE; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 6d6add76e..a7c644197 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -502,7 +502,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ #if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) - if (context->add_plt) { + if (context->plt) { const char * plt_option[2] = {"PLT=YES", NULL}; opj_encoder_set_extra_options(codec, plt_option); } From a7df096d1b03d53c9834989e3e2e5e4fb8b37f4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Mar 2023 22:39:01 +1100 Subject: [PATCH 14/19] Added release notes --- docs/releasenotes/9.5.0.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 074931671..20d7ee893 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -48,11 +48,16 @@ Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` ``corners``. This a tuple of Booleans, specifying whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. -Reading JPEG comments -^^^^^^^^^^^^^^^^^^^^^ +JPEG2000 comments and PLT marker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When opening a JPEG2000 image, the comment may now be read into -:py:attr:`~PIL.Image.Image.info`. +:py:attr:`~PIL.Image.Image.info`. The ``comment`` keyword argument can be used +to save it back again. + +If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument +is present and true when saving JPEG2000 images, tell the encoder to generate +PLT markers. Security ======== From 7d6ff23e1f493147057f062f95954b21e9eec0fb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 28 Mar 2023 07:32:30 +1100 Subject: [PATCH 15/19] Renamed "add_plt" to "plt" Co-authored-by: Hugo van Kemenade --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 11380cd55..de6b79371 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -595,7 +595,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.5.0 -**add_plt** +**plt** If ``True`` then include a PLT (packet length, tile-part header) marker segment in the produced file. The default is to not include it. From 598216fb465682646a307b930047d56adb0a42b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 19:18:50 +1100 Subject: [PATCH 16/19] OpenJPEG 2.4.0 or later is required for PLT markers --- docs/handbook/image-file-formats.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index de6b79371..74ba883b1 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -596,9 +596,9 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.5.0 **plt** - If ``True`` then include a PLT (packet length, tile-part header) marker - segment in the produced file. - The default is to not include it. + If ``True`` and OpenJPEG 2.4.0 or later is available, then include a PLT + (packet length, tile-part header) marker in the produced file. + Defaults to ``False``. .. versionadded:: 9.5.0 From 2f66d2d6a1dd307e8c28ddc5f98707acc5874cdb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 19:19:05 +1100 Subject: [PATCH 17/19] Changed maximum comment length to 65531 --- Tests/test_file_jpeg2k.py | 10 +++++++--- src/encode.c | 4 ++-- src/libImaging/Jpeg2KEncode.c | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 7b512695b..b9422c76a 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -376,14 +376,18 @@ def test_save_comment(): for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) - out.seek(0) with Image.open(out) as im: assert im.info["comment"] == b"Created by Pillow" - too_long_comment = " " * 65531 + out = BytesIO() + long_comment = b" " * 65531 + test_card.save(out, "JPEG2000", comment=long_comment) + with Image.open(out) as im: + assert im.info["comment"] == long_comment + with pytest.raises(ValueError): - test_card.save(out, "JPEG2000", comment=too_long_comment) + test_card.save(out, "JPEG2000", comment=long_comment + b" ") @pytest.mark.parametrize( diff --git a/src/encode.c b/src/encode.c index 8aa357b6c..a66594935 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,7 +1214,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; - char *comment = NULL; + char *comment; Py_ssize_t comment_size; int plt = 0; @@ -1323,7 +1323,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (comment && comment_size > 0) { /* Size is stored as as an uint16, subtract 4 bytes for the header */ - if (comment_size >= 65531) { + if (comment_size >= 65532) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 comment is too long"); diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index a7c644197..8f6370061 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -503,7 +503,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ #if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) if (context->plt) { - const char * plt_option[2] = {"PLT=YES", NULL}; + const char *plt_option[2] = {"PLT=YES", NULL}; opj_encoder_set_extra_options(codec, plt_option); } #endif From 9a7a4482195125f38c668df3555043e8d3251da0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 20:14:29 +1100 Subject: [PATCH 18/19] Increase similiarity between test_plt_marker and _parse_comment --- Tests/test_file_jpeg2k.py | 21 +++++++++++---------- src/PIL/Jpeg2KImagePlugin.py | 8 ++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b9422c76a..52e9f8853 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -411,29 +411,30 @@ def test_crashes(test_file): @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): - # Search the start of the codesteam for the PLT box (id 0xFF58) + # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: - box_bytes = out.read(2) - if not box_bytes: + marker = out.read(2) + if not marker: # End of steam encountered and no PLT or SOD break - jp2_boxid = _binary.i16be(box_bytes) + jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: - # No length specifier for main header + # SOC has no length continue elif jp2_boxid == 0xFF58: - # This is the PLT box we're looking for + # PLT return elif jp2_boxid == 0xFF93: - # SOD box encountered and no PLT, so it wasn't found + # SOD without finding PLT first break - jp2_boxlength = _binary.i16be(out.read(2)) - out.seek(jp2_boxlength - 2, os.SEEK_CUR) + hdr = out.read(2) + length = _binary.i16be(hdr) + out.seek(length - 2, os.SEEK_CUR) - # The PLT box wasn't found + # PLT wasn't found raise ValueError diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index e7d91c818..9309768ba 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -17,7 +17,7 @@ import io import os import struct -from . import Image, ImageFile +from . import Image, ImageFile, _binary class BoxReader: @@ -99,7 +99,7 @@ def _parse_codestream(fp): count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" hdr = fp.read(2) - lsiz = struct.unpack(">H", hdr)[0] + lsiz = _binary.i16be(hdr) siz = hdr + fp.read(lsiz - 2) lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( ">HHIIIIIIIIH", siz @@ -258,7 +258,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _parse_comment(self): hdr = self.fp.read(2) - length = struct.unpack(">H", hdr)[0] + length = _binary.i16be(hdr) self.fp.seek(length - 2, os.SEEK_CUR) while True: @@ -270,7 +270,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): # Start of tile or end of codestream break hdr = self.fp.read(2) - length = struct.unpack(">H", hdr)[0] + length = _binary.i16be(hdr) if typ == 0x64: # Comment self.info["comment"] = self.fp.read(length - 2)[2:] From 6d3c1985e07dfa28ae14de5bf36c711bdfc9176d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 22:18:14 +1100 Subject: [PATCH 19/19] Assert false instead of raising an error --- Tests/test_file_jpeg2k.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 52e9f8853..b6e8215f7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -418,8 +418,7 @@ def test_plt_marker(): while True: marker = out.read(2) if not marker: - # End of steam encountered and no PLT or SOD - break + assert False, "End of stream without PLT" jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: @@ -429,12 +428,8 @@ def test_plt_marker(): # PLT return elif jp2_boxid == 0xFF93: - # SOD without finding PLT first - break + assert False, "SOD without finding PLT first" hdr = out.read(2) length = _binary.i16be(hdr) out.seek(length - 2, os.SEEK_CUR) - - # PLT wasn't found - raise ValueError