diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 08e879807..8e81dacff 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -103,6 +103,31 @@ class TestFileJpeg: 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: bool, + no_default_app_segments: bool, + expect_app0: bool, + expect_app14: bool, + ) -> None: + 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) -> None: with Image.open(TEST_FILE) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 03ee96c0f..1002c8f60 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -581,6 +581,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 diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 6c7d8ea0a..634d55621 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -90,6 +90,13 @@ raised. 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 082f3551a..6526a19a3 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -807,6 +807,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: info.get("smooth", 0), optimize, info.get("keep_rgb", False), + info.get("no_default_app_segments", False), info.get("streamtype", 0), dpi, subsampling, diff --git a/src/encode.c b/src/encode.c index e56494036..c37bd2e17 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1083,6 +1083,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 */ @@ -1100,7 +1101,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnpn(nn)nnnOz#y#y#", + "ss|nnnnppn(nn)nnnOz#y#y#", &mode, &rawmode, &quality, @@ -1108,6 +1109,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &smooth, &optimize, &keep_rgb, + &no_default_app_segments, &streamtype, &xdpi, &ydpi, @@ -1194,6 +1196,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { JPEGENCODERSTATE *jpeg_encoder_state = (JPEGENCODERSTATE *)encoder->state.context; strncpy(jpeg_encoder_state->rawmode, rawmode, 8); jpeg_encoder_state->keep_rgb = keep_rgb; + jpeg_encoder_state->no_default_app_segments = no_default_app_segments; jpeg_encoder_state->quality = quality; jpeg_encoder_state->qtables = qarrays; jpeg_encoder_state->qtablesLen = qtablesLen; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 7cdba9022..0fad2f7cd 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -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; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 972435ee1..1d05e6620 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -172,6 +172,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;