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.

660894cd36 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.
This commit is contained in:
Benjamin Gilbert 2023-10-25 00:03:03 -05:00
parent 6ade47f7c0
commit 8053d5e5a0
7 changed files with 52 additions and 4 deletions

View File

@ -88,6 +88,31 @@ class TestFileJpeg:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
assert im.app["COM"] == im.info["comment"] 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,
):
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): def test_comment_write(self):
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"

View File

@ -487,6 +487,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exif** **exif**
If present, the image will be stored with the provided raw EXIF data. 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** **keep_rgb**
By default, libjpeg converts images with an RGB color space to YCbCr. 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 If this option is present and true, those images will be stored as RGB

View File

@ -26,10 +26,12 @@ TODO
API Additions 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 Security
======== ========

View File

@ -786,6 +786,7 @@ def _save(im, fp, filename):
info.get("smooth", 0), info.get("smooth", 0),
optimize, optimize,
info.get("keep_rgb", False), info.get("keep_rgb", False),
info.get("no_default_app_segments", False),
info.get("streamtype", 0), info.get("streamtype", 0),
dpi[0], dpi[0],
dpi[1], dpi[1],

View File

@ -1043,6 +1043,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
Py_ssize_t smooth = 0; Py_ssize_t smooth = 0;
Py_ssize_t optimize = 0; Py_ssize_t optimize = 0;
int keep_rgb = 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 streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
Py_ssize_t xdpi = 0, ydpi = 0; Py_ssize_t xdpi = 0, ydpi = 0;
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
@ -1060,7 +1061,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple( if (!PyArg_ParseTuple(
args, args,
"ss|nnnnpnnnnnnOz#y#y#", "ss|nnnnppnnnnnnOz#y#y#",
&mode, &mode,
&rawmode, &rawmode,
&quality, &quality,
@ -1068,6 +1069,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
&smooth, &smooth,
&optimize, &optimize,
&keep_rgb, &keep_rgb,
&no_default_app_segments,
&streamtype, &streamtype,
&xdpi, &xdpi,
&ydpi, &ydpi,
@ -1153,6 +1155,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; ((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)->quality = quality;
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;

View File

@ -77,6 +77,9 @@ typedef struct {
/* Disable automatic conversion of RGB images to YCbCr if non-zero */ /* Disable automatic conversion of RGB images to YCbCr if non-zero */
int keep_rgb; 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) */ /* Stream type (0=full, 1=tables only, 2=image only) */
int streamtype; int streamtype;

View File

@ -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 */ /* Use custom quantization tables */
if (context->qtables) { if (context->qtables) {
int i; int i;