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
from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
from PIL import (
Image,
ImageFile,
Jpeg2KImagePlugin,
UnidentifiedImageError,
_binary,
features,
)
from .helper import (
assert_image_equal,
assert_image_similar,
assert_image_similar_tofile,
skip_unless_feature,
skip_unless_feature_version,
)
EXTRA_DIR = "Tests/images/jpeg2000"
@ -364,6 +372,24 @@ def test_comment():
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(
"test_file",
[
@ -381,3 +407,29 @@ def test_crashes(test_file):
im.load()
except OSError:
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
**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::
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,
``(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
========

View File

@ -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:]
@ -351,8 +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()
plt = info.get("plt", False)
fd = -1
if hasattr(fp, "fileno"):
try:
fd = fp.fileno()
@ -374,6 +378,8 @@ def _save(im, fp, filename):
mct,
signed,
fd,
comment,
plt,
)
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;
int sgnd = 0;
Py_ssize_t fd = -1;
char *comment;
Py_ssize_t comment_size;
int plt = 0;
if (!PyArg_ParseTuple(
args,
"ss|OOOsOnOOOssbbn",
"ss|OOOsOnOOOssbbnz#p",
&mode,
&format,
&offset,
@ -1233,7 +1236,10 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
&cinema_mode,
&mct,
&sgnd,
&fd)) {
&fd,
&comment,
&comment_size,
&plt)) {
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)) {
context->quality_is_in_db = strcmp(quality_mode, "dB") == 0;
context->quality_layers = quality_layers;
@ -1332,6 +1358,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
context->cinema_mode = cine_mode;
context->mct = mct;
context->sgnd = sgnd;
context->plt = plt;
return (PyObject *)encoder;
}

View File

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

View File

@ -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;
@ -496,6 +500,14 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
opj_set_warning_handler(codec, j2k_warn, context);
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 */
if (!opj_start_compress(codec, image, stream)) {
state->errcode = IMAGING_CODEC_BROKEN;
@ -628,7 +640,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;
}