From 63c9886ed2795bf51f11f72bf28c3f4eb573e35a Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 24 Oct 2023 23:55:19 -0500 Subject: [PATCH] Allow writing RGB JPEGs libjpeg automatically converts RGB to YCbCr by default. Add a keep_colorspace option to disable libjpeg's automatic colorspace conversion during write. --- Tests/test_file_jpeg.py | 14 ++++++++++++++ docs/handbook/image-file-formats.rst | 7 +++++++ src/PIL/JpegImagePlugin.py | 1 + src/encode.c | 5 ++++- src/libImaging/Jpeg.h | 3 +++ src/libImaging/JpegEncode.c | 11 +++++++++++ 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ef070b6c5ba..df32a88e1b3 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -141,6 +141,16 @@ def test_cmyk(self): ) assert k > 0.9 + def test_rgb(self): + def getchannels(im): + return tuple(v[0] for v in im.layer) + + im = self.roundtrip(hopper()) + assert getchannels(im) == (1, 2, 3) + im = self.roundtrip(hopper(), keep_colorspace=True) + assert getchannels(im) == (ord("R"), ord("G"), ord("B")) + assert_image_similar(hopper(), im, 12) + @pytest.mark.parametrize( "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], @@ -445,6 +455,10 @@ def getsampling(im): with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") + # RGB colorspace, no subsampling by default + im = self.roundtrip(hopper(), subsampling=3, keep_colorspace=True) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + def test_exif(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index fe310df6443..87275fcdc7c 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -483,6 +483,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. +**keep_colorspace** + If present and true, indicates that the encoder should retain the + image's original color space, rather than automatically converting RGB to + YCbCr. + + .. versionadded:: 10.2.0 + **subsampling** If present, sets the subsampling for the encoder. diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 3596e089949..26c9f2da5c7 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -783,6 +783,7 @@ def validate_qtables(qtables): progressive, info.get("smooth", 0), optimize, + info.get("keep_colorspace", False), info.get("streamtype", 0), dpi[0], dpi[1], diff --git a/src/encode.c b/src/encode.c index 4664ad0f32a..596e62bcb51 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t progressive = 0; Py_ssize_t smooth = 0; Py_ssize_t optimize = 0; + int keep_colorspace = 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 */ @@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnnnOz#y#y#", + "ss|nnnnpnnnnnnOz#y#y#", &mode, &rawmode, &quality, &progressive, &smooth, &optimize, + &keep_colorspace, &streamtype, &xdpi, &ydpi, @@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); + ((JPEGENCODERSTATE *)encoder->state.context)->keep_colorspace = keep_colorspace; ((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 5cc74e69bf5..518e04880e7 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,6 +74,9 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; + /* Disable automatic colorspace conversion if nonzero */ + int keep_colorspace; + /* 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 9da830b186f..a0b9363852c 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -137,6 +137,17 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Compressor configuration */ jpeg_set_defaults(&context->cinfo); + /* Prevent RGB -> YCbCr conversion */ + if (context->keep_colorspace) { + J_COLOR_SPACE space = context->cinfo.in_color_space; +#ifdef JCS_EXTENSIONS + if (context->cinfo.in_color_space == JCS_EXT_RGBX) { + space = JCS_RGB; + } +#endif + jpeg_set_colorspace(&context->cinfo, space); + } + /* Use custom quantization tables */ if (context->qtables) { int i;