Skip to content

Commit

Permalink
Add keep_rgb option to prevent RGB -> YCbCr conversion during JPEG write
Browse files Browse the repository at this point in the history
libjpeg automatically converts RGB to YCbCr by default.  Add a keep_rgb
option to disable libjpeg's automatic conversion of RGB images during
write.
  • Loading branch information
bgilbert committed Nov 26, 2023
1 parent f9c7bd8 commit ae1d5e6
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 1 deletion.
14 changes: 14 additions & 0 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_rgb=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"],
Expand Down Expand Up @@ -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_rgb=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()
Expand Down
7 changes: 7 additions & 0 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,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_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
instead.

.. versionadded:: 10.2.0

**subsampling**
If present, sets the subsampling for the encoder.

Expand Down
1 change: 1 addition & 0 deletions src/PIL/JpegImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ def validate_qtables(qtables):
progressive,
info.get("smooth", 0),
optimize,
info.get("keep_rgb", False),
info.get("streamtype", 0),
dpi[0],
dpi[1],
Expand Down
5 changes: 4 additions & 1 deletion src/encode.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_rgb = 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 */
Expand All @@ -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_rgb,
&streamtype,
&xdpi,
&ydpi,
Expand Down Expand Up @@ -1150,6 +1152,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)->quality = quality;
((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays;
((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen;
Expand Down
3 changes: 3 additions & 0 deletions src/libImaging/Jpeg.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ typedef struct {
/* Optimize Huffman tables (slow) */
int optimize;

/* Disable automatic conversion of RGB images to YCbCr if nonzero */
int keep_rgb;

/* Stream type (0=full, 1=tables only, 2=image only) */
int streamtype;

Expand Down
14 changes: 14 additions & 0 deletions src/libImaging/JpegEncode.c
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
/* Compressor configuration */
jpeg_set_defaults(&context->cinfo);

/* Prevent RGB -> YCbCr conversion */
if (context->keep_rgb) {
switch (context->cinfo.in_color_space) {
case JCS_RGB:
#ifdef JCS_EXTENSIONS
case JCS_EXT_RGBX:
#endif
jpeg_set_colorspace(&context->cinfo, JCS_RGB);
break;
default:
break;
}
}

/* Use custom quantization tables */
if (context->qtables) {
int i;
Expand Down

0 comments on commit ae1d5e6

Please sign in to comment.