From 6ade47f7c0d5be523324ca10bff8c1b18401bfe9 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Sun, 24 Dec 2023 11:05:38 -0600 Subject: [PATCH 1/3] Add release notes for 10.3.0 --- docs/releasenotes/10.3.0.rst | 48 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 49 insertions(+) create mode 100644 docs/releasenotes/10.3.0.rst diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 00000000000..43eafd428a2 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,48 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853cc2..e86f8082b48 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 From 8053d5e5a07b0ac0615c19594f49043e45650904 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Wed, 25 Oct 2023 00:03:03 -0500 Subject: [PATCH 2/3] 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. --- Tests/test_file_jpeg.py | 25 +++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 7 +++++++ docs/releasenotes/10.3.0.rst | 8 +++++--- src/PIL/JpegImagePlugin.py | 1 + src/encode.c | 5 ++++- src/libImaging/Jpeg.h | 3 +++ src/libImaging/JpegEncode.c | 7 +++++++ 7 files changed, 52 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 979c7e33d00..2985719c24d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -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", + ( + (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" diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 276838bed1b..8de4d793172 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -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 diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 43eafd428a2..6a549385a5e 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -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 ======== diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 81b8749a332..84c1c3a818b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -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], diff --git a/src/encode.c b/src/encode.c index c7dd510150e..c82e09a7971 100644 --- a/src/encode.c +++ b/src/encode.c @@ -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 */ @@ -1060,7 +1061,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnpnnnnnnOz#y#y#", + "ss|nnnnppnnnnnnOz#y#y#", &mode, &rawmode, &quality, @@ -1068,6 +1069,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &smooth, &optimize, &keep_rgb, + &no_default_app_segments, &streamtype, &xdpi, &ydpi, @@ -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; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 7cdba902281..0fad2f7cdd6 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 00f3d5f74db..b1b5b892523 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -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; From ee6b724ad43c012f7eeb2960a4c52d07a2a2cc6f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 29 Nov 2024 07:55:02 +1100 Subject: [PATCH 3/3] Added type hints --- Tests/test_file_jpeg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index c599c0fe4c5..5c054adfd9f 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -112,11 +112,11 @@ def test_app(self) -> None: ) def test_default_app_write( self, - keep_rgb, - no_default_app_segments, - expect_app0, - expect_app14, - ): + keep_rgb: bool, + no_default_app_segments: bool, + expect_app0: bool, + expect_app14: bool, + ) -> None: im = self.roundtrip( hopper(), keep_rgb=keep_rgb,