This commit is contained in:
Benjamin Gilbert 2025-07-08 16:57:53 +00:00 committed by GitHub
commit 64a63381c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 54 additions and 1 deletions

View File

@ -103,6 +103,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: 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: def test_comment_write(self) -> None:
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

@ -581,6 +581,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

@ -90,6 +90,13 @@ raised.
API additions 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 Added PerspectiveTransform
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -807,6 +807,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
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, dpi,
subsampling, subsampling,

View File

@ -1083,6 +1083,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 */
@ -1100,7 +1101,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple( if (!PyArg_ParseTuple(
args, args,
"ss|nnnnpn(nn)nnnOz#y#y#", "ss|nnnnppn(nn)nnnOz#y#y#",
&mode, &mode,
&rawmode, &rawmode,
&quality, &quality,
@ -1108,6 +1109,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,
@ -1194,6 +1196,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
JPEGENCODERSTATE *jpeg_encoder_state = (JPEGENCODERSTATE *)encoder->state.context; JPEGENCODERSTATE *jpeg_encoder_state = (JPEGENCODERSTATE *)encoder->state.context;
strncpy(jpeg_encoder_state->rawmode, rawmode, 8); strncpy(jpeg_encoder_state->rawmode, rawmode, 8);
jpeg_encoder_state->keep_rgb = keep_rgb; 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->quality = quality;
jpeg_encoder_state->qtables = qarrays; jpeg_encoder_state->qtables = qarrays;
jpeg_encoder_state->qtablesLen = qtablesLen; jpeg_encoder_state->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

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