Skip to content

Commit ae1d5e6

Browse files
committed
Add keep_rgb option to prevent RGB -> YCbCr conversion during JPEG write
libjpeg automatically converts RGB to YCbCr by default. Add a keep_rgb option to disable libjpeg's automatic conversion of RGB images during write.
1 parent f9c7bd8 commit ae1d5e6

File tree

6 files changed

+43
-1
lines changed

6 files changed

+43
-1
lines changed

Tests/test_file_jpeg.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,16 @@ def test_cmyk(self):
141141
)
142142
assert k > 0.9
143143

144+
def test_rgb(self):
145+
def getchannels(im):
146+
return tuple(v[0] for v in im.layer)
147+
148+
im = self.roundtrip(hopper())
149+
assert getchannels(im) == (1, 2, 3)
150+
im = self.roundtrip(hopper(), keep_rgb=True)
151+
assert getchannels(im) == (ord("R"), ord("G"), ord("B"))
152+
assert_image_similar(hopper(), im, 12)
153+
144154
@pytest.mark.parametrize(
145155
"test_image_path",
146156
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
@@ -445,6 +455,10 @@ def getsampling(im):
445455
with pytest.raises(TypeError):
446456
self.roundtrip(hopper(), subsampling="1:1:1")
447457

458+
# RGB colorspace, no subsampling by default
459+
im = self.roundtrip(hopper(), subsampling=3, keep_rgb=True)
460+
assert getsampling(im) == (1, 1, 1, 1, 1, 1)
461+
448462
def test_exif(self):
449463
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
450464
info = im._getexif()

docs/handbook/image-file-formats.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
486486
**exif**
487487
If present, the image will be stored with the provided raw EXIF data.
488488

489+
**keep_rgb**
490+
By default, libjpeg converts images with an RGB color space to YCbCr.
491+
If this option is present and true, those images will be stored as RGB
492+
instead.
493+
494+
.. versionadded:: 10.2.0
495+
489496
**subsampling**
490497
If present, sets the subsampling for the encoder.
491498

src/PIL/JpegImagePlugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ def validate_qtables(qtables):
783783
progressive,
784784
info.get("smooth", 0),
785785
optimize,
786+
info.get("keep_rgb", False),
786787
info.get("streamtype", 0),
787788
dpi[0],
788789
dpi[1],

src/encode.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
10421042
Py_ssize_t progressive = 0;
10431043
Py_ssize_t smooth = 0;
10441044
Py_ssize_t optimize = 0;
1045+
int keep_rgb = 0;
10451046
Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */
10461047
Py_ssize_t xdpi = 0, ydpi = 0;
10471048
Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */
@@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
10591060

10601061
if (!PyArg_ParseTuple(
10611062
args,
1062-
"ss|nnnnnnnnnnOz#y#y#",
1063+
"ss|nnnnpnnnnnnOz#y#y#",
10631064
&mode,
10641065
&rawmode,
10651066
&quality,
10661067
&progressive,
10671068
&smooth,
10681069
&optimize,
1070+
&keep_rgb,
10691071
&streamtype,
10701072
&xdpi,
10711073
&ydpi,
@@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
11501152

11511153
strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8);
11521154

1155+
((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb;
11531156
((JPEGENCODERSTATE *)encoder->state.context)->quality = quality;
11541157
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
11551158
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;

src/libImaging/Jpeg.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ typedef struct {
7474
/* Optimize Huffman tables (slow) */
7575
int optimize;
7676

77+
/* Disable automatic conversion of RGB images to YCbCr if nonzero */
78+
int keep_rgb;
79+
7780
/* Stream type (0=full, 1=tables only, 2=image only) */
7881
int streamtype;
7982

src/libImaging/JpegEncode.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
137137
/* Compressor configuration */
138138
jpeg_set_defaults(&context->cinfo);
139139

140+
/* Prevent RGB -> YCbCr conversion */
141+
if (context->keep_rgb) {
142+
switch (context->cinfo.in_color_space) {
143+
case JCS_RGB:
144+
#ifdef JCS_EXTENSIONS
145+
case JCS_EXT_RGBX:
146+
#endif
147+
jpeg_set_colorspace(&context->cinfo, JCS_RGB);
148+
break;
149+
default:
150+
break;
151+
}
152+
}
153+
140154
/* Use custom quantization tables */
141155
if (context->qtables) {
142156
int i;

0 commit comments

Comments
 (0)