Skip to content

Commit

Permalink
Allow disabling default emission of JPEG APP0 and APP14 segments
Browse files Browse the repository at this point in the history
When embedding JPEGs into a container file format, it may be desirable
to minimize JPEG metadata, since the container will include the pertinent
details.  By default, libjpeg emits a JFIF APP0 segment for JFIF-
compatible colorspaces (grayscale or YCbCr) and Adobe APP14 otherwise.
Add a no_default_app_segments option to disable these.

660894c added code to force emission of the JFIF segment if the DPI is
specified, even for JFIF-incompatible colorspaces.  This seems
inconsistent with the JFIF spec, but apparently other software does it
too.  no_default_app_segments does not disable this behavior, since it
only happens when the application explicitly specifies the DPI.
  • Loading branch information
bgilbert committed Jan 7, 2024
1 parent 6ade47f commit b29b67e
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 4 deletions.
26 changes: 26 additions & 0 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,32 @@ 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",
(
(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,
):
out = BytesIO()
hopper().save(
out,
format="JPEG",
keep_rgb=keep_rgb,
no_default_app_segments=no_default_app_segments,
)
assert (b"\xff\xe0" in out.getvalue()) == expect_app0
assert (b"\xff\xee" in out.getvalue()) == 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
8 changes: 5 additions & 3 deletions docs/releasenotes/10.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ TODO
API Additions
=============

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

TODO
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.

Security
========
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

0 comments on commit b29b67e

Please sign in to comment.