From e3c0ee6ae22a951461a998a00118fa155a5c97a3 Mon Sep 17 00:00:00 2001 From: Kylian Ronfleux--Corail <35237015+Kyliroco@users.noreply.github.com> Date: Thu, 10 Apr 2025 07:31:05 +0000 Subject: [PATCH 1/9] Fix incorrect quality scaling when using qtables in JPEG encoder When both `qtables` and `quality` are provided, Pillow previously passed the raw `quality` value directly to `jpeg_add_quant_table()` as the scale factor, which caused incorrect quantization. This commit corrects the behavior by using `jpeg_quality_scaling(quality)` to compute the proper JPEG quantization scaling factor, consistent with `jpeg_set_quality()`. This ensures expected compression behavior when custom qtables are used alongside a specified quality level. --- src/libImaging/JpegEncode.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 3c11eac22..a2e2945bc 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -178,12 +178,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { if (context->quality != -1) { quality = context->quality; } + int scale_factor = jpeg_quality_scaling(quality); for (i = 0; i < context->qtablesLen; i++) { jpeg_add_quant_table( &context->cinfo, i, &context->qtables[i * DCTSIZE2], - quality, + scale_factor, FALSE ); context->cinfo.comp_info[i].quant_tbl_no = i; @@ -193,7 +194,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { // jpeg_set_defaults created two qtables internally, but we only // wanted one. jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], quality, FALSE + &context->cinfo, 1, &context->qtables[0], scale_factor, FALSE ); } for (i = last_q; i < context->cinfo.num_components; i++) { From 13bffef91d0406dbd394ec375e7f5e4a43411f9e Mon Sep 17 00:00:00 2001 From: Kylian Ronfleux--Corail <35237015+Kyliroco@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:01:02 +0000 Subject: [PATCH 2/9] Fix quantization table scaling when quality is applied to custom qtables When both a custom quantization table and a quality value are provided, the quantization table should be scaled using the JPEG quality scaling factor. If quality is not explicitly set, the default base quality of 50 is used to preserve the original table. This ensures consistent behavior when saving JPEGs with custom qtables. Fixes part of the issue with applying 'quality' to 'qtables'. --- src/libImaging/JpegEncode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index a2e2945bc..24e2c76ba 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -173,7 +173,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Use custom quantization tables */ if (context->qtables) { int i; - int quality = 100; + int quality = 50; int last_q = 0; if (context->quality != -1) { quality = context->quality; From 70272591f812ea29b5dd137fad64c8b87204396f Mon Sep 17 00:00:00 2001 From: Kylian Ronfleux--Corail <35237015+Kyliroco@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:59:42 +0000 Subject: [PATCH 3/9] Add test to verify that specifying standard qtables and a quality is equivalent to specifying only a quality --- Tests/test_file_jpeg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 79f0ec1a8..dfd4534fc 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -614,6 +614,10 @@ class TestFileJpeg: None ) ] + + qtable_from_qtable_quality = self.roundtrip(im, qtables={0:standard_l_qtable,1:standard_chrominance_qtable}, quality=75).quantization + qtable_from_quality = self.roundtrip(im, quality=75).quantization + assert qtable_from_qtable_quality == qtable_from_quality # list of qtable lists assert_image_similar( im, From 661f5f572905cefeb89c8328052b6d44c5eb54d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:00:33 +0000 Subject: [PATCH 4/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index dfd4534fc..a13fe4edf 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -614,8 +614,12 @@ class TestFileJpeg: None ) ] - - qtable_from_qtable_quality = self.roundtrip(im, qtables={0:standard_l_qtable,1:standard_chrominance_qtable}, quality=75).quantization + + qtable_from_qtable_quality = self.roundtrip( + im, + qtables={0: standard_l_qtable, 1: standard_chrominance_qtable}, + quality=75, + ).quantization qtable_from_quality = self.roundtrip(im, quality=75).quantization assert qtable_from_qtable_quality == qtable_from_quality # list of qtable lists From 5d82c79c2fae756b2b0c637c403e5985489dd1d4 Mon Sep 17 00:00:00 2001 From: Kylian Ronfleux--Corail <35237015+Kyliroco@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:06:33 +0000 Subject: [PATCH 5/9] add test to qtables to test all quantization tables for quality beetween 0 and 100 --- Tests/test_file_jpeg.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index a13fe4edf..12e56d0b8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -614,14 +614,18 @@ class TestFileJpeg: None ) ] + for quality in range(101): - qtable_from_qtable_quality = self.roundtrip( - im, - qtables={0: standard_l_qtable, 1: standard_chrominance_qtable}, - quality=75, - ).quantization - qtable_from_quality = self.roundtrip(im, quality=75).quantization - assert qtable_from_qtable_quality == qtable_from_quality + qtable_from_qtable_quality = self.roundtrip( + im, + qtables={0: standard_l_qtable, 1: standard_chrominance_qtable}, + quality=quality, + ).quantization + + qtable_from_quality = self.roundtrip(im, quality=quality).quantization + + assert qtable_from_qtable_quality == qtable_from_quality + # list of qtable lists assert_image_similar( im, From 32590baeecea1ed1ef76a2e65d9aed8230f6d15a Mon Sep 17 00:00:00 2001 From: Kylian Ronfleux--Corail <35237015+Kyliroco@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:07:11 +0000 Subject: [PATCH 6/9] add force_baseline which limit output of quantization table to 255 when giving a quality and a qantization table --- src/libImaging/JpegEncode.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 24e2c76ba..ec6fa0ea2 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -175,8 +175,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { int i; int quality = 50; int last_q = 0; + boolean force_baseline = FALSE; if (context->quality != -1) { quality = context->quality; + force_baseline = TRUE; } int scale_factor = jpeg_quality_scaling(quality); for (i = 0; i < context->qtablesLen; i++) { @@ -185,7 +187,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { i, &context->qtables[i * DCTSIZE2], scale_factor, - FALSE + force_baseline ); context->cinfo.comp_info[i].quant_tbl_no = i; last_q = i; @@ -194,7 +196,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { // jpeg_set_defaults created two qtables internally, but we only // wanted one. jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], scale_factor, FALSE + &context->cinfo, 1, &context->qtables[0], scale_factor, force_baseline ); } for (i = last_q; i < context->cinfo.num_components; i++) { From 45fb0626b30ce630c734c95c93276235367803ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:10:25 +0000 Subject: [PATCH 7/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg.py | 2 +- src/libImaging/JpegEncode.c | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 12e56d0b8..c45d4c596 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -625,7 +625,7 @@ class TestFileJpeg: qtable_from_quality = self.roundtrip(im, quality=quality).quantization assert qtable_from_qtable_quality == qtable_from_quality - + # list of qtable lists assert_image_similar( im, diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index ec6fa0ea2..6e8887f1e 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -196,7 +196,11 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { // jpeg_set_defaults created two qtables internally, but we only // wanted one. jpeg_add_quant_table( - &context->cinfo, 1, &context->qtables[0], scale_factor, force_baseline + &context->cinfo, + 1, + &context->qtables[0], + scale_factor, + force_baseline ); } for (i = last_q; i < context->cinfo.num_components; i++) { From aca38b65043083c44e04dbd12074ce5ce89bbfbb Mon Sep 17 00:00:00 2001 From: Kylian Ronfleux--Corail <35237015+Kyliroco@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:31:43 +0000 Subject: [PATCH 8/9] add more information on how qtables parameter works with quality --- docs/handbook/image-file-formats.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bfa462c04..a557207f1 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -557,6 +557,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: hardly any gain in image quality. The value ``keep`` is only valid for JPEG files and will retain the original image quality level, subsampling, and qtables. + For more information on how qtables are modified based on the quality parameter, + see the qtables section. **optimize** If present and true, indicates that the encoder should make an extra pass @@ -621,6 +623,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: * a list, tuple, or dictionary (with integer keys = range(len(keys))) of lists of 64 integers. There must be between 2 and 4 tables. + + If a quality parameter is provided, the qtables will be adjusted accordingly. + By default, the qtables are based on a standard JPEG table with a quality of 50. + The qtable values will be reduced if the quality is higher than 50 and increased + if the quality is lower than 50. .. versionadded:: 2.5.0 From 630265f79a91205f4e1a9c1eef6e51d219af184c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 08:33:29 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/handbook/image-file-formats.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a557207f1..d2c1ce6d6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -557,7 +557,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: hardly any gain in image quality. The value ``keep`` is only valid for JPEG files and will retain the original image quality level, subsampling, and qtables. - For more information on how qtables are modified based on the quality parameter, + For more information on how qtables are modified based on the quality parameter, see the qtables section. **optimize** @@ -623,10 +623,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: * a list, tuple, or dictionary (with integer keys = range(len(keys))) of lists of 64 integers. There must be between 2 and 4 tables. - - If a quality parameter is provided, the qtables will be adjusted accordingly. - By default, the qtables are based on a standard JPEG table with a quality of 50. - The qtable values will be reduced if the quality is higher than 50 and increased + + If a quality parameter is provided, the qtables will be adjusted accordingly. + By default, the qtables are based on a standard JPEG table with a quality of 50. + The qtable values will be reduced if the quality is higher than 50 and increased if the quality is lower than 50. .. versionadded:: 2.5.0