Merge pull request #6903 from joshware/jp2k_options

Support custom comments and PLT markers when saving JPEG2000 images
This commit is contained in:
Andrew Murray 2023-03-29 23:43:54 +11:00 committed by GitHub
commit f8be09612d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 137 additions and 11 deletions

View File

@ -4,13 +4,21 @@ from io import BytesIO
import pytest import pytest
from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features from PIL import (
Image,
ImageFile,
Jpeg2KImagePlugin,
UnidentifiedImageError,
_binary,
features,
)
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
skip_unless_feature, skip_unless_feature,
skip_unless_feature_version,
) )
EXTRA_DIR = "Tests/images/jpeg2000" EXTRA_DIR = "Tests/images/jpeg2000"
@ -364,6 +372,24 @@ def test_comment():
pass pass
def test_save_comment():
for comment in ("Created by Pillow", b"Created by Pillow"):
out = BytesIO()
test_card.save(out, "JPEG2000", comment=comment)
with Image.open(out) as im:
assert im.info["comment"] == b"Created by Pillow"
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=long_comment + b" ")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_file", "test_file",
[ [
@ -381,3 +407,29 @@ def test_crashes(test_file):
im.load() im.load()
except OSError: except OSError:
pass pass
@skip_unless_feature_version("jpg_2000", "2.4.0")
def test_plt_marker():
# 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:
marker = out.read(2)
if not marker:
assert False, "End of stream without PLT"
jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F:
# SOC has no length
continue
elif jp2_boxid == 0xFF58:
# PLT
return
elif jp2_boxid == 0xFF93:
assert False, "SOD without finding PLT first"
hdr = out.read(2)
length = _binary.i16be(hdr)
out.seek(length - 2, os.SEEK_CUR)

View File

@ -589,6 +589,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
.. versionadded:: 9.1.0 .. versionadded:: 9.1.0
**comment**
Adds a custom comment to the file, replacing the default
"Created by OpenJPEG version" comment.
.. versionadded:: 9.5.0
**plt**
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
.. note:: .. note::
To enable JPEG 2000 support, you need to build and install the OpenJPEG To enable JPEG 2000 support, you need to build and install the OpenJPEG

View File

@ -48,11 +48,16 @@ Added ``corners`` argument to ``ImageDraw.rounded_rectangle()``
``corners``. This a tuple of Booleans, specifying whether to round each corner, ``corners``. This a tuple of Booleans, specifying whether to round each corner,
``(top_left, top_right, bottom_right, bottom_left)``. ``(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 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 Security
======== ========

View File

@ -17,7 +17,7 @@ import io
import os import os
import struct import struct
from . import Image, ImageFile from . import Image, ImageFile, _binary
class BoxReader: class BoxReader:
@ -99,7 +99,7 @@ def _parse_codestream(fp):
count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
hdr = fp.read(2) hdr = fp.read(2)
lsiz = struct.unpack(">H", hdr)[0] lsiz = _binary.i16be(hdr)
siz = hdr + fp.read(lsiz - 2) siz = hdr + fp.read(lsiz - 2)
lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from(
">HHIIIIIIIIH", siz ">HHIIIIIIIIH", siz
@ -258,7 +258,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
def _parse_comment(self): def _parse_comment(self):
hdr = self.fp.read(2) hdr = self.fp.read(2)
length = struct.unpack(">H", hdr)[0] length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR) self.fp.seek(length - 2, os.SEEK_CUR)
while True: while True:
@ -270,7 +270,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
# Start of tile or end of codestream # Start of tile or end of codestream
break break
hdr = self.fp.read(2) hdr = self.fp.read(2)
length = struct.unpack(">H", hdr)[0] length = _binary.i16be(hdr)
if typ == 0x64: if typ == 0x64:
# Comment # Comment
self.info["comment"] = self.fp.read(length - 2)[2:] self.info["comment"] = self.fp.read(length - 2)[2:]
@ -351,8 +351,12 @@ def _save(im, fp, filename):
cinema_mode = info.get("cinema_mode", "no") cinema_mode = info.get("cinema_mode", "no")
mct = info.get("mct", 0) mct = info.get("mct", 0)
signed = info.get("signed", False) signed = info.get("signed", False)
fd = -1 comment = info.get("comment")
if isinstance(comment, str):
comment = comment.encode()
plt = info.get("plt", False)
fd = -1
if hasattr(fp, "fileno"): if hasattr(fp, "fileno"):
try: try:
fd = fp.fileno() fd = fp.fileno()
@ -374,6 +378,8 @@ def _save(im, fp, filename):
mct, mct,
signed, signed,
fd, fd,
comment,
plt,
) )
ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)])

View File

@ -1214,10 +1214,13 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
char mct = 0; char mct = 0;
int sgnd = 0; int sgnd = 0;
Py_ssize_t fd = -1; Py_ssize_t fd = -1;
char *comment;
Py_ssize_t comment_size;
int plt = 0;
if (!PyArg_ParseTuple( if (!PyArg_ParseTuple(
args, args,
"ss|OOOsOnOOOssbbn", "ss|OOOsOnOOOssbbnz#p",
&mode, &mode,
&format, &format,
&offset, &offset,
@ -1233,7 +1236,10 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
&cinema_mode, &cinema_mode,
&mct, &mct,
&sgnd, &sgnd,
&fd)) { &fd,
&comment,
&comment_size,
&plt)) {
return NULL; return NULL;
} }
@ -1315,6 +1321,26 @@ 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 >= 65532) {
PyErr_SetString(
PyExc_ValueError,
"JPEG 2000 comment is too long");
Py_DECREF(encoder);
return NULL;
}
char *p = malloc(comment_size + 1);
if (!p) {
Py_DECREF(encoder);
return ImagingError_MemoryError();
}
memcpy(p, comment, comment_size);
p[comment_size] = '\0';
context->comment = p;
}
if (quality_layers && PySequence_Check(quality_layers)) { if (quality_layers && PySequence_Check(quality_layers)) {
context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; context->quality_is_in_db = strcmp(quality_mode, "dB") == 0;
context->quality_layers = quality_layers; context->quality_layers = quality_layers;
@ -1332,6 +1358,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
context->cinema_mode = cine_mode; context->cinema_mode = cine_mode;
context->mct = mct; context->mct = mct;
context->sgnd = sgnd; context->sgnd = sgnd;
context->plt = plt;
return (PyObject *)encoder; return (PyObject *)encoder;
} }

View File

@ -97,6 +97,12 @@ typedef struct {
/* PRIVATE CONTEXT (set by decoder) */ /* PRIVATE CONTEXT (set by decoder) */
const char *error_msg; const char *error_msg;
/* Custom comment */
char *comment;
/* Include PLT marker segment */
int plt;
} JPEG2KENCODESTATE; } JPEG2KENCODESTATE;
/* /*

View File

@ -439,6 +439,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
params.tcp_mct = context->mct; params.tcp_mct = context->mct;
} }
if (context->comment) {
params.cp_comment = context->comment;
}
params.prog_order = context->progression; params.prog_order = context->progression;
params.cp_cinema = context->cinema_mode; params.cp_cinema = context->cinema_mode;
@ -496,6 +500,14 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
opj_set_warning_handler(codec, j2k_warn, context); opj_set_warning_handler(codec, j2k_warn, context);
opj_setup_encoder(codec, &params, image); opj_setup_encoder(codec, &params, 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->plt) {
const char *plt_option[2] = {"PLT=YES", NULL};
opj_encoder_set_extra_options(codec, plt_option);
}
#endif
/* Start encoding */ /* Start encoding */
if (!opj_start_compress(codec, image, stream)) { if (!opj_start_compress(codec, image, stream)) {
state->errcode = IMAGING_CODEC_BROKEN; state->errcode = IMAGING_CODEC_BROKEN;
@ -628,7 +640,12 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) {
free((void *)context->error_msg); free((void *)context->error_msg);
} }
if (context->comment) {
free((void *)context->comment);
}
context->error_msg = NULL; context->error_msg = NULL;
context->comment = NULL;
return -1; return -1;
} }