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

Allow disabling default emission of JPEG APP0 and APP14 segments #7679

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ def test_app(self):
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
assert im.app["COM"] == im.info["comment"]

@pytest.mark.parametrize(
"keep_rgb, no_default_app_segments, expect_app0, expect_app14",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a case with dpi? (JFIF should present as documentation said)

(
(False, False, True, False),
(True, False, False, True),
(False, True, False, False),
(True, True, False, False),
),
)
def test_default_app_write(
self,
keep_rgb,
no_default_app_segments,
expect_app0,
expect_app14,
):
im = self.roundtrip(
hopper(),
keep_rgb=keep_rgb,
no_default_app_segments=no_default_app_segments,
)
markers = {m[0] for m in im.applist}
assert ("APP0" in markers) == expect_app0
assert ("APP14" in markers) == expect_app14

def test_comment_write(self):
with Image.open(TEST_FILE) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
Expand Down
7 changes: 7 additions & 0 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exif**
If present, the image will be stored with the provided raw EXIF data.

**no_default_app_segments**
If present and true, the image is stored without default JFIF and Adobe
application segments. The JFIF segment will still be stored if **dpi**
is also specified.

.. versionadded:: 10.3.0

**keep_rgb**
By default, libjpeg converts images with an RGB color space to YCbCr.
If this option is present and true, those images will be stored as RGB
Expand Down
7 changes: 7 additions & 0 deletions docs/releasenotes/10.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ TODO
API Additions
=============

JPEG app segments
^^^^^^^^^^^^^^^^^

When saving JPEG files, ``no_default_app_segments`` can now be set to ``True`` to store
the image without default JFIF and Adobe application segments. The JFIF segment will
still be stored if ``dpi`` is also specified.

Added PerspectiveTransform
^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
1 change: 1 addition & 0 deletions src/PIL/JpegImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ def validate_qtables(qtables):
info.get("smooth", 0),
optimize,
info.get("keep_rgb", False),
info.get("no_default_app_segments", False),
info.get("streamtype", 0),
dpi[0],
dpi[1],
Expand Down
5 changes: 4 additions & 1 deletion src/encode.c
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
Py_ssize_t smooth = 0;
Py_ssize_t optimize = 0;
int keep_rgb = 0;
int no_default_app_segments = 0;
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
Py_ssize_t xdpi = 0, ydpi = 0;
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
Expand All @@ -1060,14 +1061,15 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {

if (!PyArg_ParseTuple(
args,
"ss|nnnnpnnnnnnOz#y#y#",
"ss|nnnnppnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
&progressive,
&smooth,
&optimize,
&keep_rgb,
&no_default_app_segments,
&streamtype,
&xdpi,
&ydpi,
Expand Down Expand Up @@ -1153,6 +1155,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);

((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb;
((JPEGENCODERSTATE *)encoder->state.context)->no_default_app_segments = no_default_app_segments;
((JPEGENCODERSTATE *)encoder->state.context)->quality = quality;
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;
Expand Down
3 changes: 3 additions & 0 deletions src/libImaging/Jpeg.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ typedef struct {
/* Disable automatic conversion of RGB images to YCbCr if non-zero */
int keep_rgb;

/* Disable default application segments if non-zero */
int no_default_app_segments;

/* Stream type (0=full, 1=tables only, 2=image only) */
int streamtype;

Expand Down
7 changes: 7 additions & 0 deletions src/libImaging/JpegEncode.c
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
}

/* Disable app markers if the colorspace enabled them.
xdpi/ydpi will still override this. */
if (context->no_default_app_segments) {
context->cinfo.write_JFIF_header = FALSE;
context->cinfo.write_Adobe_marker = FALSE;
}

/* Use custom quantization tables */
if (context->qtables) {
int i;
Expand Down