mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 01:46:18 +03:00
Merge pull request #6903 from joshware/jp2k_options
Support custom comments and PLT markers when saving JPEG2000 images
This commit is contained in:
commit
f8be09612d
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
========
|
||||
|
|
|
@ -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)])
|
||||
|
|
31
src/encode.c
31
src/encode.c
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/*
|
||||
|
|
|
@ -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, ¶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->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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user