From 6db0843af4e0c048fa4c26283b5df5d438b617c0 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 12 Sep 2020 20:15:33 +0200 Subject: [PATCH 1/6] Add support for 16-bit precision JPEG quantization values Don't force JPEG quantization to be baseline-compatible Quantization values will not be limited to values 1..255 and may be 16 bits if needed. This may cause compatibility issues. --- src/PIL/JpegImagePlugin.py | 20 +++++++++++--------- src/libImaging/JpegEncode.c | 7 +++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6145e5da7..7d2a8fdc6 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -36,6 +36,7 @@ import io import os import struct import subprocess +import sys import tempfile import warnings @@ -244,15 +245,16 @@ def DQT(self, marker): n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) while len(s): - if len(s) < 65: - raise SyntaxError("bad quantization table marker") v = i8(s[0]) - if v // 16 == 0: - self.quantization[v & 15] = array.array("B", s[1:65]) - s = s[65:] - else: - return # FIXME: add code to read 16-bit tables! - # raise SyntaxError, "bad quantization table element size" + precision = 1 if (v // 16 == 0) else 2 # in bytes + qt_length = 1 + precision * 64 + if len(s) < qt_length: + raise SyntaxError("bad quantization table marker") + data = array.array("B" if precision == 1 else "H", s[1:qt_length]) + if sys.byteorder == "little" and precision > 1: + data.byteswap() # the values are always big-endian + self.quantization[v & 15] = data + s = s[qt_length:] # @@ -683,7 +685,7 @@ def _save(im, fp, filename): try: if len(table) != 64: raise TypeError - table = array.array("B", table) + table = array.array("H", table) except TypeError as e: raise ValueError("Invalid quantization table") from e else: diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index b255025fa..0f73666f1 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -159,22 +159,21 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) quality = context->quality; } for (i = 0; i < context->qtablesLen; i++) { - // TODO: Should add support for none baseline jpeg_add_quant_table(&context->cinfo, i, &context->qtables[i * DCTSIZE2], - quality, TRUE); + quality, FALSE); context->cinfo.comp_info[i].quant_tbl_no = i; last_q = i; } if (context->qtablesLen == 1) { // jpeg_set_defaults created two qtables internally, but we only wanted one. jpeg_add_quant_table(&context->cinfo, 1, &context->qtables[0], - quality, TRUE); + quality, FALSE); } for (i = last_q; i < context->cinfo.num_components; i++) { context->cinfo.comp_info[i].quant_tbl_no = last_q; } } else if (context->quality != -1) { - jpeg_set_quality(&context->cinfo, context->quality, 1); + jpeg_set_quality(&context->cinfo, context->quality, FALSE); } /* Set subsampling options */ From 0a235314b8ea9b763a61013db2c494e62ab179b2 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 10 Oct 2020 15:26:44 +0200 Subject: [PATCH 2/6] Remove outdated comment about 8-bit DQT support --- src/PIL/JpegImagePlugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 7d2a8fdc6..06222e904 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -235,9 +235,8 @@ def SOF(self, marker): def DQT(self, marker): # - # Define quantization table. Support baseline 8-bit tables - # only. Note that there might be more than one table in - # each marker. + # Define quantization table. Note that there might be more + # than one table in each marker. # FIXME: The quantization tables can be used to estimate the # compression quality. From b212a1eb8f24bea4db8df4173830cdab0cf0ba56 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 10 Oct 2020 16:04:18 +0200 Subject: [PATCH 3/6] Keep baseline compatibility when specifying quality 16-bit quantization tables can appear at quality values below about 25. These may cause compatibility problems. Maintain baseline compatibility and avoid confusing users with warning messages. --- 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 0f73666f1..ab730d92d 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -173,7 +173,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->cinfo.comp_info[i].quant_tbl_no = last_q; } } else if (context->quality != -1) { - jpeg_set_quality(&context->cinfo, context->quality, FALSE); + jpeg_set_quality(&context->cinfo, context->quality, TRUE); } /* Set subsampling options */ From 2f0544b54289b2d85fde51213620db2c92b24d69 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 10 Oct 2020 18:53:49 +0200 Subject: [PATCH 4/6] Add tests for loading/saving of 16-bit quantization tables --- Tests/test_file_jpeg.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 78d588dec..25cfb35e6 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -446,6 +446,7 @@ class TestFileJpeg: assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization + assert reloaded.quantization[0].typecode == 'B' with Image.open("Tests/images/hopper.jpg") as im: qtables = im.quantization @@ -544,6 +545,30 @@ class TestFileJpeg: with pytest.raises(ValueError): self.roundtrip(im, qtables=[[1, 2, 3, 4]]) + def test_load_16bit_qtables(self): + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert len(im.quantization) == 2 + assert len(im.quantization[0]) == 64 + assert max(im.quantization[0]) > 255 + + def test_save_multiple_16bit_qtables(self): + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + im2 = self.roundtrip(im, qtables='keep') + assert im.quantization == im2.quantization + + def test_save_single_16bit_qtable(self): + with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + im2 = self.roundtrip(im, qtables=[im.quantization[0]]) + assert len(im2.quantization) == 1 + assert im2.quantization[0] == im.quantization[0] + + def test_save_low_quality_baseline_qtables(self): + with Image.open(TEST_FILE) as im: + im2 = self.roundtrip(im, quality=10) + assert len(im2.quantization) == 2 + assert max(im2.quantization[0]) <= 255 + assert max(im2.quantization[1]) <= 255 + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: From d54dc4062fd80c2d97ebd1f50e6ddabe0b53ab43 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:01:28 +0200 Subject: [PATCH 5/6] fixup! Add tests for loading/saving of 16-bit quantization tables --- Tests/test_file_jpeg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 25cfb35e6..ff469d15c 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -446,7 +446,7 @@ class TestFileJpeg: assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization - assert reloaded.quantization[0].typecode == 'B' + assert reloaded.quantization[0].typecode == "B" with Image.open("Tests/images/hopper.jpg") as im: qtables = im.quantization @@ -553,12 +553,12 @@ class TestFileJpeg: def test_save_multiple_16bit_qtables(self): with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: - im2 = self.roundtrip(im, qtables='keep') + im2 = self.roundtrip(im, qtables="keep") assert im.quantization == im2.quantization def test_save_single_16bit_qtable(self): with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: - im2 = self.roundtrip(im, qtables=[im.quantization[0]]) + im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] From 938e251088fe122ed753f123710803a8bb15599e Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:08:39 +0200 Subject: [PATCH 6/6] Add new JPEG test image --- Tests/images/hopper_16bit_qtables.jpg | Bin 0 -> 2044 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Tests/images/hopper_16bit_qtables.jpg diff --git a/Tests/images/hopper_16bit_qtables.jpg b/Tests/images/hopper_16bit_qtables.jpg new file mode 100644 index 0000000000000000000000000000000000000000..88abef943b996929c7a1094873156ddf6b1f0ea6 GIT binary patch literal 2044 zcmcJMdpO%!8pqEs7aB{eR!Z9>b8#{JR? zT0|;h&AOJh(`^t#)2d5Q*Os~z+wIm+hBEfonSFMjXaCw~pJ&heeBbw+_q@-0&gY!{ z@%>q#?&;>?1|SFk2sW@k1=7`l6EFvszz&uvJhuZ@uv`HZo_l~Wzz0m&Ghhn?--8U0 z1O5Q#UH~ar1xsKGj?aJ`Y(jV_0{R4wXvharf-}z`Ye)cn3a?_JB!~y)LOk#WB0|m3 z5cD0yf%@V4D{wyb!zFO=ajvk10e@Jn@R3Z=3Z;TQxcMHG`v156PJmMc9KbY6nqT76B_zufPW4MfkdGdFp5f8Ww@YC9Uvei5`jXZ(I^yL{YQ8opm1mnLo!uC z(?0@pI1X>c&9724I?>d5fIjrr*g7&kO$n=Ykf5z&VroVtncLXfeR|B^!R2#TH+Pzc zX8u zZf^^5f~?2?(pe5-hw{by|2o!3;Un5LKQ5@&sK^?h6pI$B^k7|iVr zu|yvX(AGW^elgv%@9`EF|JR>S;N<0up&7Sj4Qe}&Ji%&698dmoJPV+8hHJbrQGPJwV*%r4(!HTy8_!yj^L zl|9si3d7VDFXxs6mNS~sSPGUsgOqG@R|yw(0$j^(eowcp=jNxorMD2G;t{VF^)SNV zLs_?}7)(Q$kH2qQ7%d`;G+tA0OsviVzcHj0HK&(QIHH>qd0+jkTDc%+R=l8)CoeMO z)1Ty{3|@hSl(Cc&hK)`t6SIAfPV>kspl;zMsn_n!X0_lLhw{wHx#R~vm=&7Q)Oy0^ zpB^_+CJ!D%F=SJm!(wwS^yPCUf}Hvl7FP((+E)=m4&?Ws7Zk|SYkB%O*izYPfrzC zS`VCOkvh2;WPXobYnpW#mc+7fIa-}XM3l*%&fO}Ygq%|ko);PlvA)BV4?0tp{@nFm zpf6~kFPlf}s<=m*>)I|3Hkr-r#@sjk;iKAzE&;Ujl8X%Lm`9N9f@c4T`>GCwd$SN8 zMd_;lX@ssdcfIOw-sds`wpinq!PpteE=f5&oT2R4Avr{!yW%5V&y`iAEqvRx$BH3D zsdk^gT;Uyhgr`5}iVsjPq7OOa)fMuo%igE?27dF%=$CZ{Q&#B{R~=Y<_s=RGTs_;b z**M&dSHhO(w8}`qU!7z-+CL`SnpE}3K2FJ~(EWXW=-^VR$*}N}%V?6QG=B6FO)Ap9 zw0Uex+@Nh{pp#iuok$Rax+wiqWtyXnq|MFkZVGiUWyGPVG=cZ^f}B<&v)Ra(SIrNX zdIfo~EE#)qS;eP91xN<5HZFLuTHNDEf^ys+#G=vNeLKV3vjFFrGw;Om z?(5t>UcOP;fYa@gC82UdzBD4DGoZSV)IyH)j@-B~2`PWhX>QW