Skip to content

Commit 0e87938

Browse files
committed
Allow disabling default emission of JPEG APP0 and APP14 segments
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.
1 parent 4b9de58 commit 0e87938

File tree

7 files changed

+53
-4
lines changed

7 files changed

+53
-4
lines changed

Tests/test_file_jpeg.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,32 @@ def test_app(self):
8888
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
8989
assert im.app["COM"] == im.info["comment"]
9090

91+
@pytest.mark.parametrize(
92+
"keep_rgb, no_default_app_segments, expect_app0, expect_app14",
93+
(
94+
(False, False, True, False),
95+
(True, False, False, True),
96+
(False, True, False, False),
97+
(True, True, False, False),
98+
),
99+
)
100+
def test_default_app_write(
101+
self,
102+
keep_rgb,
103+
no_default_app_segments,
104+
expect_app0,
105+
expect_app14,
106+
):
107+
out = BytesIO()
108+
hopper().save(
109+
out,
110+
format="JPEG",
111+
keep_rgb=keep_rgb,
112+
no_default_app_segments=no_default_app_segments,
113+
)
114+
assert (b"\xff\xe0" in out.getvalue()) == expect_app0
115+
assert (b"\xff\xee" in out.getvalue()) == expect_app14
116+
91117
def test_comment_write(self):
92118
with Image.open(TEST_FILE) as im:
93119
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"

docs/handbook/image-file-formats.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
487487
**exif**
488488
If present, the image will be stored with the provided raw EXIF data.
489489

490+
**no_default_app_segments**
491+
If present and true, the image is stored without default JFIF and Adobe
492+
application segments. The JFIF segment will still be stored if **dpi**
493+
is also specified.
494+
495+
.. versionadded:: 10.3.0
496+
490497
**keep_rgb**
491498
By default, libjpeg converts images with an RGB color space to YCbCr.
492499
If this option is present and true, those images will be stored as RGB

docs/releasenotes/10.3.0.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ TODO
2626
API Additions
2727
=============
2828

29-
TODO
30-
^^^^
29+
JPEG app segments
30+
^^^^^^^^^^^^^^^^^
3131

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

3436
Security
3537
========

src/PIL/JpegImagePlugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,7 @@ def validate_qtables(qtables):
786786
info.get("smooth", 0),
787787
optimize,
788788
info.get("keep_rgb", False),
789+
info.get("no_default_app_segments", False),
789790
info.get("streamtype", 0),
790791
dpi[0],
791792
dpi[1],

src/encode.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
10431043
Py_ssize_t smooth = 0;
10441044
Py_ssize_t optimize = 0;
10451045
int keep_rgb = 0;
1046+
int no_default_app_segments = 0;
10461047
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
10471048
Py_ssize_t xdpi = 0, ydpi = 0;
10481049
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
@@ -1060,14 +1061,15 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
10601061

10611062
if (!PyArg_ParseTuple(
10621063
args,
1063-
"ss|nnnnpnnnnnnOz#y#y#",
1064+
"ss|nnnnppnnnnnnOz#y#y#",
10641065
&mode,
10651066
&rawmode,
10661067
&quality,
10671068
&progressive,
10681069
&smooth,
10691070
&optimize,
10701071
&keep_rgb,
1072+
&no_default_app_segments,
10711073
&streamtype,
10721074
&xdpi,
10731075
&ydpi,
@@ -1153,6 +1155,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
11531155
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
11541156

11551157
((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb;
1158+
((JPEGENCODERSTATE *)encoder->state.context)->no_default_app_segments = no_default_app_segments;
11561159
((JPEGENCODERSTATE *)encoder->state.context)->quality = quality;
11571160
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
11581161
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;

src/libImaging/Jpeg.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ typedef struct {
7777
/* Disable automatic conversion of RGB images to YCbCr if nonzero */
7878
int keep_rgb;
7979

80+
/* Disable default application segments if nonzero */
81+
int no_default_app_segments;
82+
8083
/* Stream type (0=full, 1=tables only, 2=image only) */
8184
int streamtype;
8285

src/libImaging/JpegEncode.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
161161
}
162162
}
163163

164+
/* Disable app markers if the colorspace enabled them.
165+
xdpi/ydpi will still override this. */
166+
if (context->no_default_app_segments) {
167+
context->cinfo.write_JFIF_header = FALSE;
168+
context->cinfo.write_Adobe_marker = FALSE;
169+
}
170+
164171
/* Use custom quantization tables */
165172
if (context->qtables) {
166173
int i;

0 commit comments

Comments
 (0)