From f17f1bc6074adc657dda54f6dca750e1d9fa80cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Apr 2020 20:43:49 +1000 Subject: [PATCH 01/34] Added method argument to single frame WebP saving --- Tests/test_file_webp.py | 73 +++++++++++++--------------- src/PIL/WebPImagePlugin.py | 2 + src/_webp.c | 97 ++++++++++++++++++++++++-------------- 3 files changed, 98 insertions(+), 74 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 1b8aa9f8a..f538b0ecf 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,3 +1,5 @@ +import io + import pytest from PIL import Image, WebPImagePlugin @@ -54,15 +56,10 @@ class TestFileWebp: # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - def test_write_rgb(self, tmp_path): - """ - Can we write a RGB mode file to webp without error. - Does it have the bits we expect? - """ - + def _roundtrip(self, tmp_path, mode, epsilon, args={}): temp_file = str(tmp_path / "temp.webp") - hopper(self.rgb_mode).save(temp_file) + hopper(mode).save(temp_file, **args) with Image.open(temp_file) as image: assert image.mode == self.rgb_mode assert image.size == (128, 128) @@ -70,18 +67,38 @@ class TestFileWebp: image.load() image.getdata() - # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm - assert_image_similar_tofile( - image, "Tests/images/hopper_webp_write.ppm", 12.0 - ) + if mode == self.rgb_mode: + # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm + assert_image_similar_tofile( + image, "Tests/images/hopper_webp_write.ppm", 12.0 + ) # This test asserts that the images are similar. If the average pixel # difference between the two images is less than the epsilon value, # then we're going to accept that it's a reasonable lossy version of - # the image. The old lena images for WebP are showing ~16 on - # Ubuntu, the jpegs are showing ~18. - target = hopper(self.rgb_mode) - assert_image_similar(image, target, 12.0) + # the image. + target = hopper(mode) + if mode != self.rgb_mode: + target = target.convert(self.rgb_mode) + assert_image_similar(image, target, epsilon) + + def test_write_rgb(self, tmp_path): + """ + Can we write a RGB mode file to webp without error? + Does it have the bits we expect? + """ + + self._roundtrip(tmp_path, self.rgb_mode, 12.5) + + def test_write_method(self, tmp_path): + self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) + + buffer_no_args = io.BytesIO() + hopper().save(buffer_no_args, format="WEBP") + + buffer_method = io.BytesIO() + hopper().save(buffer_method, format="WEBP", method=6) + assert buffer_no_args.getbuffer() != buffer_method.getbuffer() def test_write_unsupported_mode_L(self, tmp_path): """ @@ -89,18 +106,7 @@ class TestFileWebp: similar to the original file. """ - temp_file = str(tmp_path / "temp.webp") - hopper("L").save(temp_file) - with Image.open(temp_file) as image: - assert image.mode == self.rgb_mode - assert image.size == (128, 128) - assert image.format == "WEBP" - - image.load() - image.getdata() - target = hopper("L").convert(self.rgb_mode) - - assert_image_similar(image, target, 10.0) + self._roundtrip(tmp_path, "L", 10.0) def test_write_unsupported_mode_P(self, tmp_path): """ @@ -108,18 +114,7 @@ class TestFileWebp: similar to the original file. """ - temp_file = str(tmp_path / "temp.webp") - hopper("P").save(temp_file) - with Image.open(temp_file) as image: - assert image.mode == self.rgb_mode - assert image.size == (128, 128) - assert image.format == "WEBP" - - image.load() - image.getdata() - target = hopper("P").convert(self.rgb_mode) - - assert_image_similar(image, target, 50.0) + self._roundtrip(tmp_path, "P", 50.0) def test_WebPEncode_with_invalid_args(self): """ diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index eda685508..6c4f7decf 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -325,6 +325,7 @@ def _save(im, fp, filename): if isinstance(exif, Image.Exif): exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") + method = im.encoderinfo.get("method", 0) if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -342,6 +343,7 @@ def _save(im, fp, filename): float(quality), im.mode, icc_profile, + method, exif, xmp, ) diff --git a/src/_webp.c b/src/_webp.c index babea60f3..dcb6c85cb 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -545,6 +545,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) int height; int lossless; float quality_factor; + int method; uint8_t* rgb; uint8_t* icc_bytes; uint8_t* exif_bytes; @@ -556,49 +557,75 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) Py_ssize_t exif_size; Py_ssize_t xmp_size; size_t ret_size; + int rgba_mode; + int channels; + int ok; ImagingSectionCookie cookie; + WebPConfig config; + WebPMemoryWriter writer; + WebPPicture pic; - if (!PyArg_ParseTuple(args, "y#iiifss#s#s#", + if (!PyArg_ParseTuple(args, "y#iiifss#is#s#", (char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode, - &icc_bytes, &icc_size, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { + &icc_bytes, &icc_size, &method, &exif_bytes, &exif_size, &xmp_bytes, &xmp_size)) { return NULL; } - if (strcmp(mode, "RGBA")==0){ - if (size < width * height * 4){ - Py_RETURN_NONE; - } - #if WEBP_ENCODER_ABI_VERSION >= 0x0100 - if (lossless) { - ImagingSectionEnter(&cookie); - ret_size = WebPEncodeLosslessRGBA(rgb, width, height, 4 * width, &output); - ImagingSectionLeave(&cookie); - } else - #endif - { - ImagingSectionEnter(&cookie); - ret_size = WebPEncodeRGBA(rgb, width, height, 4 * width, quality_factor, &output); - ImagingSectionLeave(&cookie); - } - } else if (strcmp(mode, "RGB")==0){ - if (size < width * height * 3){ - Py_RETURN_NONE; - } - #if WEBP_ENCODER_ABI_VERSION >= 0x0100 - if (lossless) { - ImagingSectionEnter(&cookie); - ret_size = WebPEncodeLosslessRGB(rgb, width, height, 3 * width, &output); - ImagingSectionLeave(&cookie); - } else - #endif - { - ImagingSectionEnter(&cookie); - ret_size = WebPEncodeRGB(rgb, width, height, 3 * width, quality_factor, &output); - ImagingSectionLeave(&cookie); - } - } else { + + rgba_mode = strcmp(mode, "RGBA") == 0; + if (!rgba_mode && strcmp(mode, "RGB") != 0) { Py_RETURN_NONE; } + channels = rgba_mode ? 4 : 3; + if (size < width * height * channels) { + Py_RETURN_NONE; + } + + // Setup config for this frame + if (!WebPConfigInit(&config)) { + PyErr_SetString(PyExc_RuntimeError, "failed to initialize config!"); + return NULL; + } + config.lossless = lossless; + config.quality = quality_factor; + config.method = method; + + // Validate the config + if (!WebPValidateConfig(&config)) { + PyErr_SetString(PyExc_ValueError, "invalid configuration"); + return NULL; + } + + if (!WebPPictureInit(&pic)) { + PyErr_SetString(PyExc_ValueError, "could not initialise picture"); + return NULL; + } + pic.width = width; + pic.height = height; + pic.use_argb = 1; // Don't convert RGB pixels to YUV + + if (rgba_mode) { + WebPPictureImportRGBA(&pic, rgb, channels * width); + } else { + WebPPictureImportRGB(&pic, rgb, channels * width); + } + + WebPMemoryWriterInit(&writer); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &writer; + + ImagingSectionEnter(&cookie); + ok = WebPEncode(&config, &pic); + ImagingSectionLeave(&cookie); + + WebPPictureFree(&pic); + if (!ok) { + PyErr_SetString(PyExc_ValueError, "encoding error"); + return NULL; + } + output = writer.mem; + ret_size = writer.size; + #ifndef HAVE_WEBPMUX if (ret_size > 0) { PyObject *ret = PyBytes_FromStringAndSize((char*)output, ret_size); From e10cab42f1112645e1cf90b0e460e441e2682f1f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Apr 2020 20:56:17 +1000 Subject: [PATCH 02/34] Consider transparency when drawing text on an RGBA image --- Tests/images/transparent_background_text.png | Bin 0 -> 1271 bytes Tests/test_image_paste.py | 2 +- Tests/test_imagefont.py | 12 ++++++++++++ src/libImaging/Paste.c | 8 ++++++-- 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 Tests/images/transparent_background_text.png diff --git a/Tests/images/transparent_background_text.png b/Tests/images/transparent_background_text.png new file mode 100644 index 0000000000000000000000000000000000000000..40acd92b62243953ebc0910629922eef112424e1 GIT binary patch literal 1271 zcmeAS@N?(olHy`uVBq!ia0y~yVAKJ!Q#jawqzxnQ0R{$^eV#6kAr*7p-j44O4V5|Y zaeb1ailSHjRfP~GB~2xzo}kkUbedi$F5RZ1(b?3*rNp7svT4Dmh4vy-_g?W1V_7;e zY(_>vOH{xk=T}*_SK8jW<>mdWvPi%A(d_%Zz5m^|yl3ZoWBdHfnf{&D&*y!fEjdBO za}p+6^4QZQB=niTwv<-&MP2Ou{8{s5{nOWKf8*HW zGCxZM1y7E8^Iy$?`3CEr=oRrSG0b-uY;MoJ&}Y56q4mJt1M_BWD=(VKx8~syTlfDA z?tP^KR)t%w4hJRwJ+OxD?Q@1|?XIU07Bjl92#4}U)E?M=ApQWu2RnoHi;}1B zTkga;NLBcqEKN0D6ZUwfzm!cG<8CN^u zp8GfV-G2EU8vkoqH?)@Yi9by~qjWZAm*Dx72dN31nPM!On)h@J@10nBVCs_h*(%rH zs@)B!klC^LM3zpYCY*4>VDmLH4X zdYnGXR=47^na|@0McY@K_O2H6WPPj0c|6-b)ACDYf4_0u?*>cWThA~2D-yW$CP}M6 z?}N5xrtiJyKbNH5DQNw;nn>+15746fXt6pX_wk>Vey(Vz1ESm%I2qEVmk!GpC1%cYTT9E4RUn zG5$%B?MtOOPP>A4G)+0YYr%0tk*|~GOmDbIIV^ipB>Xb(_T`d)bFSDukPG^kb*1Xk z+)KA+dW(MClBAota;Mx5o#RI4$19}W_umc3meO?GKl>A7#45i!^*6H*S!dqndf%0+ zbLG8W<@6lScg7Ne^=?P{Ix5tZo?ZDeTT0M5@P5L(%X$AVZqiUPdy?D!={~pg9_8y^ zmC8zWIxFwX3$A{hV|=eduW#{vZk=DcN@`cXze{AhWHL|?~z<)l;>pT3T@ wmAj{^ZuIT%u-)miK6g51E~c|cU;cllW|6Y9@p)IxfCUmdKI;Vst0LA-JqyPW_ literal 0 HcmV?d00001 diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 1d3ca8135..3740fbcdc 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -236,7 +236,7 @@ class TestImagingPaste: [ (127, 191, 254, 191), (111, 207, 206, 110), - (127, 254, 127, 0), + (255, 255, 255, 0) if mode == "RGBA" else (127, 254, 127, 0), (207, 207, 239, 239), (191, 191, 190, 191), (207, 206, 111, 112), diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 0e642cde2..b9dec9530 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -150,6 +150,18 @@ class TestImageFont: assert_image_equal(img_path, img_filelike) + def test_transparent_background(self): + im = Image.new(mode="RGBA", size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = self.get_font() + + txt = "Hello World!" + draw.text((10, 10), txt, font=ttf) + + target = "Tests/images/transparent_background_text.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 4.09) + def test_textsize_equal(self): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 0bda25739..69353ce46 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -379,9 +379,13 @@ fill_mask_L(Imaging imOut, const UINT8* ink, Imaging imMask, UINT8* mask = (UINT8*) imMask->image[y+sy]+sx; for (x = 0; x < xsize; x++) { for (i = 0; i < pixelsize; i++) { - *out = BLEND(*mask, *out, ink[i], tmp1); - out++; + UINT8 channel_mask = *mask; + if (strcmp(imOut->mode, "RGBA") == 0 && i != 3) { + channel_mask = 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); + } + out[i] = BLEND(channel_mask, out[i], ink[i], tmp1); } + out += pixelsize; mask++; } } From 9390e5636a21ef6dbd68f787eb975bf40b255fa9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Apr 2020 18:53:37 +1000 Subject: [PATCH 03/34] Also consider other alpha modes --- src/libImaging/Paste.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 69353ce46..d0610cfc5 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -380,7 +380,13 @@ fill_mask_L(Imaging imOut, const UINT8* ink, Imaging imMask, for (x = 0; x < xsize; x++) { for (i = 0; i < pixelsize; i++) { UINT8 channel_mask = *mask; - if (strcmp(imOut->mode, "RGBA") == 0 && i != 3) { + if (( + strcmp(imOut->mode, "RGBa") == 0 || + strcmp(imOut->mode, "RGBA") == 0 || + strcmp(imOut->mode, "La") == 0 || + strcmp(imOut->mode, "LA") == 0 || + strcmp(imOut->mode, "PA") == 0 + ) && i != 3) { channel_mask = 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); } out[i] = BLEND(channel_mask, out[i], ink[i], tmp1); From 5728662c7f93997f9d0e4d588e44437d45d880da Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 9 May 2020 09:29:40 +0200 Subject: [PATCH 04/34] add support for CF_DIBV5, CF_HDROP, and 'PNG' in ImageGrab.grabclipboard() on win32 --- Tests/test_imagegrab.py | 25 ++++++++++++++++- src/PIL/ImageGrab.py | 24 ++++++++++++---- src/display.c | 62 +++++++++++++++++++++++++++++++---------- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 82e746fda..9b5084acf 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -4,7 +4,7 @@ import sys import pytest from PIL import Image, ImageGrab -from .helper import assert_image +from .helper import assert_image, assert_image_equal_tofile class TestImageGrab: @@ -71,3 +71,26 @@ $bmp = New-Object Drawing.Bitmap 200, 200 im = ImageGrab.grabclipboard() assert_image(im, im.mode, im.size) + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_file(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') + p.communicate() + + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, "Tests/images/hopper.gif") + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_png(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write( + rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") +$ms = new-object System.IO.MemoryStream(, $bytes) +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +[Windows.Forms.Clipboard]::SetData("PNG", $ms)""" + ) + p.communicate() + + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, "Tests/images/hopper.png") diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 39d5e23c7..9fd8b1610 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -93,12 +93,24 @@ def grabclipboard(): os.unlink(filepath) return im elif sys.platform == "win32": - data = Image.core.grabclipboard_win32() - if isinstance(data, bytes): - from . import BmpImagePlugin - import io + import io - return BmpImagePlugin.DibImageFile(io.BytesIO(data)) - return data + fmt, data = Image.core.grabclipboard_win32() + if isinstance(data, str): + if fmt == "file": + with open(data, "rb") as f: + im = Image.open(io.BytesIO(f.read())) + return im + if isinstance(data, bytes): + data = io.BytesIO(data) + if fmt == "png": + from . import PngImagePlugin + + return PngImagePlugin.PngImageFile(data) + elif fmt == "DIB": + from . import BmpImagePlugin + + return BmpImagePlugin.DibImageFile(data) + return None else: raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") diff --git a/src/display.c b/src/display.c index 9a337d1c0..0d52ca2cf 100644 --- a/src/display.c +++ b/src/display.c @@ -32,6 +32,7 @@ #ifdef _WIN32 +#include #include "ImDib.h" #if SIZEOF_VOID_P == 8 @@ -473,33 +474,66 @@ PyObject* PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args) { int clip; - HANDLE handle; + HANDLE handle = NULL; int size; void* data; PyObject* result; + UINT format; + UINT formats[] = { CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0 }; + LPCSTR format_names[] = { "DIB", "DIB", "file", "png", NULL }; - clip = OpenClipboard(NULL); - /* FIXME: check error status */ - - handle = GetClipboardData(CF_DIB); - if (!handle) { - /* FIXME: add CF_HDROP support to allow cut-and-paste from - the explorer */ - CloseClipboard(); - Py_INCREF(Py_None); - return Py_None; + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } + + // find best format as set by clipboard owner + format = 0; + while (!handle && (format = EnumClipboardFormats(format))) { + for (UINT i = 0; formats[i] != 0; i++) { + if (format == formats[i]) { + handle = GetClipboardData(format); + format = i; + break; + } + } + } + + if (!handle) { + CloseClipboard(); + return Py_BuildValue("zO", NULL, Py_None); + } + + if (formats[format] == CF_HDROP) { + LPDROPFILES files = (LPDROPFILES)GlobalLock(handle); + size = GlobalSize(handle); + + if (files->fWide) { + LPCWSTR filename = (LPCWSTR)(((char*)files) + files->pFiles); + size = wcsnlen(filename, (size - files->pFiles) / 2); + result = Py_BuildValue("zu#", "file", filename, size); + } + else { + LPCSTR filename = (LPCSTR)(((char*)files) + files->pFiles); + size = strnlen(filename, size - files->pFiles); + result = Py_BuildValue("zs#", "file", filename, size); + } + + GlobalUnlock(handle); + CloseClipboard(); + + return result; } - size = GlobalSize(handle); data = GlobalLock(handle); + size = GlobalSize(handle); result = PyBytes_FromStringAndSize(data, size); GlobalUnlock(handle); - CloseClipboard(); - return result; + return Py_BuildValue("zN", format_names[format], result); } /* -------------------------------------------------------------------- */ From 1656edaf4176eea36b3acac0f8410b2a204493a3 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 9 May 2020 10:40:10 +0200 Subject: [PATCH 05/34] fix docs compliance for CF_HDROP --- Tests/test_imagegrab.py | 4 +++- src/PIL/ImageGrab.py | 19 ++++++++++++------- src/display.c | 22 ---------------------- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 9b5084acf..23eee2445 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,3 +1,4 @@ +import os import subprocess import sys @@ -79,7 +80,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200 p.communicate() im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, "Tests/images/hopper.gif") + assert len(im) == 1 + assert os.path.samefile(im[0], "Tests/images/hopper.gif") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_png(self): diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 9fd8b1610..cb685b1ed 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -93,15 +93,20 @@ def grabclipboard(): os.unlink(filepath) return im elif sys.platform == "win32": - import io - fmt, data = Image.core.grabclipboard_win32() - if isinstance(data, str): - if fmt == "file": - with open(data, "rb") as f: - im = Image.open(io.BytesIO(f.read())) - return im + if fmt == "file": # CF_HDROP + import struct + + o = struct.unpack_from("I", data)[0] + if data[16] != 0: + files = data[o:].decode("utf-16le").split("\0") + return files[: files.index("")] + else: + files = data[o:].decode("mbcs").split("\0") + return files[: files.index("")] if isinstance(data, bytes): + import io + data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin diff --git a/src/display.c b/src/display.c index 0d52ca2cf..c8d3086e3 100644 --- a/src/display.c +++ b/src/display.c @@ -32,7 +32,6 @@ #ifdef _WIN32 -#include #include "ImDib.h" #if SIZEOF_VOID_P == 8 @@ -504,27 +503,6 @@ PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args) return Py_BuildValue("zO", NULL, Py_None); } - if (formats[format] == CF_HDROP) { - LPDROPFILES files = (LPDROPFILES)GlobalLock(handle); - size = GlobalSize(handle); - - if (files->fWide) { - LPCWSTR filename = (LPCWSTR)(((char*)files) + files->pFiles); - size = wcsnlen(filename, (size - files->pFiles) / 2); - result = Py_BuildValue("zu#", "file", filename, size); - } - else { - LPCSTR filename = (LPCSTR)(((char*)files) + files->pFiles); - size = strnlen(filename, size - files->pFiles); - result = Py_BuildValue("zs#", "file", filename, size); - } - - GlobalUnlock(handle); - CloseClipboard(); - - return result; - } - data = GlobalLock(handle); size = GlobalSize(handle); From 228301373f5b1adacf775dff26ff69bb10b5efa6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 May 2020 06:57:15 +1000 Subject: [PATCH 06/34] Fixed comparison warnings --- src/libImaging/Jpeg.h | 2 +- src/libImaging/Jpeg2KEncode.c | 6 +++--- src/libImaging/QuantPngQuant.c | 4 ++-- src/libImaging/QuantPngQuant.h | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index df6d8a903..280b6d638 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -110,7 +110,7 @@ typedef struct { int extra_offset; - int rawExifLen; /* EXIF data length */ + size_t rawExifLen; /* EXIF data length */ char* rawExif; /* EXIF buffer pointer */ } JPEGENCODERSTATE; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 49ef5e254..5b18e472c 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -50,7 +50,7 @@ static OPJ_SIZE_T j2k_write(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) { ImagingCodecState state = (ImagingCodecState)p_user_data; - int result; + unsigned int result; result = _imaging_write_pyFd(state->fd, p_buffer, p_nb_bytes); @@ -399,8 +399,8 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) Py_ssize_t n; float *pq; - if (len) { - if (len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { + if (len > 0) { + if ((unsigned)len > sizeof(params.tcp_rates) / sizeof(params.tcp_rates[0])) { len = sizeof(params.tcp_rates)/sizeof(params.tcp_rates[0]); } diff --git a/src/libImaging/QuantPngQuant.c b/src/libImaging/QuantPngQuant.c index ef40b282b..753ceb02f 100644 --- a/src/libImaging/QuantPngQuant.c +++ b/src/libImaging/QuantPngQuant.c @@ -20,8 +20,8 @@ int quantize_pngquant( Pixel *pixelData, - int width, - int height, + unsigned int width, + unsigned int height, uint32_t quantPixels, Pixel **palette, uint32_t *paletteLength, diff --git a/src/libImaging/QuantPngQuant.h b/src/libImaging/QuantPngQuant.h index d539a7a0d..fb0b4cc03 100644 --- a/src/libImaging/QuantPngQuant.h +++ b/src/libImaging/QuantPngQuant.h @@ -4,8 +4,8 @@ #include "QuantTypes.h" int quantize_pngquant(Pixel *, - int, - int, + unsigned int, + unsigned int, uint32_t, Pixel **, uint32_t *, From b3604167ad84329a41e615cdd10b627651a3c640 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 May 2020 20:47:57 +1000 Subject: [PATCH 07/34] Change STRIPBYTECOUNTS to LONG if necessary when saving --- Tests/test_file_tiff_metadata.py | 17 +++++++++++++++++ src/PIL/TiffImagePlugin.py | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 9fe601bd6..876f790cf 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -156,6 +156,23 @@ def test_write_metadata(tmp_path): assert value == reloaded[tag], "%s didn't roundtrip" % tag +def test_change_stripbytecounts_tag_type(tmp_path): + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.tif") as im: + info = im.tag_v2 + + # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT + im = im.resize((500, 500)) + + # STRIPBYTECOUNTS can be a SHORT or a LONG + info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT + + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG + + def test_no_duplicate_50741_tag(): assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 333437625..43a2f7c40 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1508,7 +1508,10 @@ def _save(im, fp, filename): # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) ifd[ROWSPERSTRIP] = im.size[1] - ifd[STRIPBYTECOUNTS] = stride * im.size[1] + stripByteCounts = stride * im.size[1] + if stripByteCounts >= 2 ** 16: + ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG + ifd[STRIPBYTECOUNTS] = stripByteCounts ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer # no compression by default: ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) From aa1761bc9fa31c3d7c7eba9d8e1986e6d6fdabe4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 May 2020 22:37:13 +1000 Subject: [PATCH 08/34] Replace tiff_jpeg with jpeg compression when saving --- Tests/test_file_libtiff.py | 8 ++++++++ src/PIL/TiffImagePlugin.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9523e5901..6e82a7986 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -448,6 +448,14 @@ class TestFileLibTiff(LibTiffTestCase): assert size_compressed > size_jpeg assert size_jpeg > size_jpeg_30 + def test_tiff_jpeg_compression(self, tmp_path): + im = hopper("RGB") + out = str(tmp_path / "temp.tif") + im.save(out, compression="tiff_jpeg") + + with Image.open(out) as reloaded: + assert reloaded.info["compression"] == "jpeg" + def test_quality(self, tmp_path): im = hopper("RGB") out = str(tmp_path / "temp.tif") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 333437625..0ffcbb093 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1427,6 +1427,9 @@ def _save(im, fp, filename): compression = im.encoderinfo.get("compression", im.info.get("compression")) if compression is None: compression = "raw" + elif compression == "tiff_jpeg": + # OJPEG is obsolete, so use new-style JPEG compression instead + compression = "jpeg" libtiff = WRITE_LIBTIFF or compression != "raw" From 660894cd367b79e1280cea8fa67536416d1a9138 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 24 May 2020 23:58:30 +1000 Subject: [PATCH 09/34] Write JFIF header when saving JPEG --- Tests/test_file_jpeg.py | 18 ++++++++++-------- src/libImaging/JpegEncode.c | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 08db11645..616bb4dd8 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -91,15 +91,17 @@ class TestFileJpeg: assert k > 0.9 def test_dpi(self): - def test(xdpi, ydpi=None): - with Image.open(TEST_FILE) as im: - im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) - return im.info.get("dpi") + for test_image_path in [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"]: - assert test(72) == (72, 72) - assert test(300) == (300, 300) - assert test(100, 200) == (100, 200) - assert test(0) is None # square pixels + def test(xdpi, ydpi=None): + with Image.open(test_image_path) as im: + im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) + return im.info.get("dpi") + + assert test(72) == (72, 72) + assert test(300) == (300, 300) + assert test(100, 200) == (100, 200) + assert test(0) is None # square pixels def test_icc(self, tmp_path): # Test ICC support diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 8882b61be..b255025fa 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -222,6 +222,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) context->cinfo.smoothing_factor = context->smooth; context->cinfo.optimize_coding = (boolean) context->optimize; if (context->xdpi > 0 && context->ydpi > 0) { + context->cinfo.write_JFIF_header = TRUE; context->cinfo.density_unit = 1; /* dots per inch */ context->cinfo.X_density = context->xdpi; context->cinfo.Y_density = context->ydpi; From 696aa7972df42dbd0408b79c3b5178d515845be1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 May 2020 07:15:20 +1000 Subject: [PATCH 10/34] Parametrized test --- Tests/test_file_jpeg.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 616bb4dd8..258908871 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -90,18 +90,19 @@ class TestFileJpeg: ] assert k > 0.9 - def test_dpi(self): - for test_image_path in [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"]: + @pytest.mark.parametrize( + "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], + ) + def test_dpi(self, test_image_path): + def test(xdpi, ydpi=None): + with Image.open(test_image_path) as im: + im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) + return im.info.get("dpi") - def test(xdpi, ydpi=None): - with Image.open(test_image_path) as im: - im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) - return im.info.get("dpi") - - assert test(72) == (72, 72) - assert test(300) == (300, 300) - assert test(100, 200) == (100, 200) - assert test(0) is None # square pixels + assert test(72) == (72, 72) + assert test(300) == (300, 300) + assert test(100, 200) == (100, 200) + assert test(0) is None # square pixels def test_icc(self, tmp_path): # Test ICC support From 9067e68e64dbb04fe3fd920ee29d60d40ec26f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 27 May 2020 22:43:06 +1000 Subject: [PATCH 11/34] Corrected undefined behaviour --- src/libImaging/QuantOctree.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index fa45ae707..e1205acc3 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -28,6 +28,7 @@ #include #include +#include "ImagingUtils.h" #include "QuantOctree.h" typedef struct _ColorBucket{ @@ -152,10 +153,10 @@ static void avg_color_from_color_bucket(const ColorBucket bucket, Pixel *dst) { float count = bucket->count; if (count != 0) { - dst->c.r = (int)(bucket->r / count); - dst->c.g = (int)(bucket->g / count); - dst->c.b = (int)(bucket->b / count); - dst->c.a = (int)(bucket->a / count); + dst->c.r = CLIP8((int)(bucket->r / count)); + dst->c.g = CLIP8((int)(bucket->g / count)); + dst->c.b = CLIP8((int)(bucket->b / count)); + dst->c.a = CLIP8((int)(bucket->a / count)); } else { dst->c.r = 0; dst->c.g = 0; From d2e23e386b7be9775aba20471067caac0f217d46 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 28 May 2020 12:07:53 +0100 Subject: [PATCH 12/34] simplify code Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index cb685b1ed..d16dd08f6 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -100,10 +100,9 @@ def grabclipboard(): o = struct.unpack_from("I", data)[0] if data[16] != 0: files = data[o:].decode("utf-16le").split("\0") - return files[: files.index("")] else: files = data[o:].decode("mbcs").split("\0") - return files[: files.index("")] + return files[: files.index("")] if isinstance(data, bytes): import io From 9fbd35fe87e16b9bded90b5c4682cfc86f5fe4d6 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 27 May 2020 23:21:32 +0200 Subject: [PATCH 13/34] use mode for getsize --- src/PIL/ImageFont.py | 6 ++++-- src/_imagingft.c | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 25ceaa16a..98b6ab66e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -259,7 +259,7 @@ class FreeTypeFont: :return: (width, height) """ - size, offset = self.font.getsize(text, direction, features, language) + size, offset = self.font.getsize(text, False, direction, features, language) return ( size[0] + stroke_width * 2 + offset[0], size[1] + stroke_width * 2 + offset[1], @@ -468,7 +468,9 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - size, offset = self.font.getsize(text, direction, features, language) + size, offset = self.font.getsize( + text, mode == "1", direction, features, language + ) size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 im = fill("L", size, 0) self.font.render( diff --git a/src/_imagingft.c b/src/_imagingft.c index 9fe189c5f..4f5456373 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -605,6 +605,8 @@ font_getsize(FontObject* self, PyObject* args) FT_Face face; int xoffset, yoffset; int horizontal_dir; + int mask = 0; + int load_flags; const char *dir = NULL; const char *lang = NULL; size_t i, count; @@ -614,11 +616,11 @@ font_getsize(FontObject* self, PyObject* args) /* calculate size and bearing for a given string */ PyObject* string; - if (!PyArg_ParseTuple(args, "O|zOz:getsize", &string, &dir, &features, &lang)) { + if (!PyArg_ParseTuple(args, "O|izOz:getsize", &string, &mask, &dir, &features, &lang)) { return NULL; } - count = text_layout(string, self, dir, features, lang, &glyph_info, 0); + count = text_layout(string, self, dir, features, lang, &glyph_info, mask); if (PyErr_Occurred()) { return NULL; } @@ -637,7 +639,11 @@ font_getsize(FontObject* self, PyObject* args) /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 * Yifu Yu, 2014-10-15 */ - error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); + load_flags = FT_LOAD_NO_BITMAP; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + error = FT_Load_Glyph(face, index, load_flags); if (error) { return geterror(error); } From 2dd9324df210a60f663cd3c4d3795ee2457babc0 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 1 Jun 2020 19:21:40 +0200 Subject: [PATCH 14/34] add mono color text test --- Tests/images/text_mono.gif | Bin 0 -> 1560 bytes Tests/test_imagefont.py | 17 +++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 Tests/images/text_mono.gif diff --git a/Tests/images/text_mono.gif b/Tests/images/text_mono.gif new file mode 100644 index 0000000000000000000000000000000000000000..b350c10e64a2f14c9220af9d49eca19a955431c5 GIT binary patch literal 1560 zcmXYwcRbZ!9LCQ@+`@HB%679uh-~E=Wy?tRDkCE!Nujz$5kgT((#W1_nk(MhphS#KeTfVvim@dhFOSW@hH&$B(nHu$(w?;^fJb ztgNhTY;5f8>>L~%oSd9oTwL7T+&nxyI2`WOsZ+eXynK9o{QUd^0s?}9f~TNIdU|?!d3k$#`}p|Wx^>If*Z21A+kSq2ckbNr_xHbh_wK!W_W}X}0s{ksf`abf zzyILDgNF|v5(tDxj~+dK{5Uu`I3y(G$&)8fpFRx@4Gjwmd-m*EczAe3L_}m{@$%)%*x1;(xVTrZUcG+(`pug+@$vC*-@Z*qNOC>mrpFgLkr)OkjWM*b&Wo2b&XXoVPDz7Yinz7Z|~^n=gww5 z?*8%PM^8^rZ*Om3UtfQJ|G>b&;NalU(9rPk@W{x>&!0a>M@PrT#>U6T$z<}cU%w_M zCMG8*fB*hXp-`r#rlzN-XJ%$*XJ_Z;=H}<;7Zw&47Z;b7mX?>7S5{V5S6Bc1`LnjR zw!Xf;v9Ynaxw*BqwY|N)v$ON}@88|s-MziN{r&xa|Ngh+AeE?8sPOUs* zbugM)#JVZ(V{IswPbX0;Ke;}Fm5mk+h$)Q#1;RMlaDWgiOicqw1x^G2uC*{=0ipdJ ziV6%iOXC5+2P4Dj-EdggrllyO=d)po&U>BW%)Tu1LbuTt7I)~X31@_^p+e{~Zz5bJ z_N1F++2$w`<_7?viPz@<1W@Wr4Y0%#FZjLDJQ#o{Og5nanpB|x04iCR4&VqI^Kf{u zgNv}WOsr}x*Bn<{uC8>tM6Z=6)82YFH3{9H2 z7|AB%j7XX>P{LVALFu$9B2ijlpSDzZWgt-jlDm3bAUOmB%ae>!5zcV$tVz02nn9Xn z-<)Z-MX@JkxNqJp&!Ij_z^Z@2qR6!`*J-4G(W=ykq6S@fPys`u0ghw`i_riqNfP#< z0_ZMQ7!U<$hzzhQlssqw4#q%fz*~U5Kn5(2*a!c&-vniT%TuEqct8dr^h}1F`=r%L zR2;Q^hcd|*bf?e}F{JVhFog0L=O!tE6*6@sond7VSO>jXOkLch!0lB^XEYBiwzq>6 z8&@I0^;j`hxGdxdJRz?*z#M|c@LoVeB#bySa~1~T)r9v#=5L7;G0Q}xws Date: Tue, 5 May 2020 21:20:59 +1000 Subject: [PATCH 15/34] BYTE tags of variable length are only single strings --- Tests/test_file_tiff_metadata.py | 8 ++++---- src/PIL/TiffImagePlugin.py | 6 ++++-- src/encode.c | 26 +++++++++----------------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 9fe601bd6..338d8d4fe 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -319,13 +319,13 @@ def test_empty_values(): def test_PhotoshopInfo(tmp_path): with Image.open("Tests/images/issue_2278.tif") as im: - assert len(im.tag_v2[34377]) == 1 - assert isinstance(im.tag_v2[34377][0], bytes) + assert len(im.tag_v2[34377]) == 70 + assert isinstance(im.tag_v2[34377], bytes) out = str(tmp_path / "temp.tiff") im.save(out) with Image.open(out) as reloaded: - assert len(reloaded.tag_v2[34377]) == 1 - assert isinstance(reloaded.tag_v2[34377][0], bytes) + assert len(reloaded.tag_v2[34377]) == 70 + assert isinstance(reloaded.tag_v2[34377], bytes) def test_too_many_entries(): diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 8e7570062..a18621d23 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -573,8 +573,10 @@ class ImageFileDirectory_v2(MutableMapping): # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. # Don't mess with the legacy api, since it's frozen. - if (info.length == 1) or ( - info.length is None and len(values) == 1 and not legacy_api + if ( + (info.length == 1) + or self.tagtype[tag] == TiffTags.BYTE + or (info.length is None and len(values) == 1 and not legacy_api) ): # Don't mess with the legacy api, since it's frozen. if legacy_api and self.tagtype[tag] in [ diff --git a/src/encode.c b/src/encode.c index 1d463e9c4..16d45e8f0 100644 --- a/src/encode.c +++ b/src/encode.c @@ -790,28 +790,24 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) if (!is_core_tag) { // Register field for non core tags. + if (type == TIFF_BYTE) { + is_var_length = 1; + } if (ImagingLibTiffMergeFieldInfo(&encoder->state, type, key_int, is_var_length)) { continue; } } - if (is_var_length) { + if (type == TIFF_BYTE) { + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) key_int, + PyBytes_Size(value), PyBytes_AsString(value)); + } else if (is_var_length) { Py_ssize_t len,i; TRACE(("Setting from Tuple: %d \n", key_int)); len = PyTuple_Size(value); - if (type == TIFF_BYTE) { - UINT8 *av; - /* malloc check ok, calloc checks for overflow */ - av = calloc(len, sizeof(UINT8)); - if (av) { - for (i=0;istate, (ttag_t) key_int, len, av); - free(av); - } - } else if (type == TIFF_SHORT) { + if (type == TIFF_SHORT) { UINT16 *av; /* malloc check ok, calloc checks for overflow */ av = calloc(len, sizeof(UINT16)); @@ -914,10 +910,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, (FLOAT64)PyFloat_AsDouble(value)); - } else if (type == TIFF_BYTE) { - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) key_int, - (UINT8)PyLong_AsLong(value)); } else if (type == TIFF_SBYTE) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, From 859b27572bc3581d5bb7be4db654c93abc9cb132 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 3 May 2020 19:41:38 +1000 Subject: [PATCH 16/34] Removed forcing of BYTE to ASCII --- Tests/test_file_libtiff.py | 17 ++++++++++++++++- src/encode.c | 3 +-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9523e5901..f1ad18828 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -299,7 +299,11 @@ class TestFileLibTiff(LibTiffTestCase): ) continue - if libtiff and isinstance(value, bytes): + if ( + libtiff + and isinstance(value, bytes) + and isinstance(tiffinfo, dict) + ): value = value.decode() assert reloaded_value == value @@ -322,6 +326,17 @@ class TestFileLibTiff(LibTiffTestCase): ) TiffImagePlugin.WRITE_LIBTIFF = False + def test_xmlpacket_tag(self, tmp_path): + TiffImagePlugin.WRITE_LIBTIFF = True + + out = str(tmp_path / "temp.tif") + hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) + TiffImagePlugin.WRITE_LIBTIFF = False + + with Image.open(out) as reloaded: + if 700 in reloaded.tag_v2: + assert reloaded.tag_v2[700] == b"xmlpacket tag" + def test_int_dpi(self, tmp_path): # issue #1765 im = hopper("RGB") diff --git a/src/encode.c b/src/encode.c index 16d45e8f0..6506edb96 100644 --- a/src/encode.c +++ b/src/encode.c @@ -761,8 +761,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } } - if (PyBytes_Check(value) && - (type == TIFF_BYTE || type == TIFF_UNDEFINED)) { + if (PyBytes_Check(value) && type == TIFF_UNDEFINED) { // For backwards compatibility type = TIFF_ASCII; } From 2d284aea12f1d81b702305b34bd1d4c8e082034e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 6 May 2020 20:12:59 +1000 Subject: [PATCH 17/34] Allow writing of UNDEFINED tags --- Tests/test_file_libtiff.py | 27 ++++++++++++++++++++------- src/PIL/TiffImagePlugin.py | 16 +++++++++------- src/encode.c | 7 +------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f1ad18828..855a9ab3a 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -299,13 +299,6 @@ class TestFileLibTiff(LibTiffTestCase): ) continue - if ( - libtiff - and isinstance(value, bytes) - and isinstance(tiffinfo, dict) - ): - value = value.decode() - assert reloaded_value == value # Test with types @@ -682,6 +675,26 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False assert icc == icc_libtiff + def test_write_icc(self, tmp_path): + def check_write(libtiff): + TiffImagePlugin.WRITE_LIBTIFF = libtiff + + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc_profile = img.info["icc_profile"] + + out = str(tmp_path / "temp.tif") + img.save(out, icc_profile=icc_profile) + with Image.open(out) as reloaded: + assert icc_profile == reloaded.info["icc_profile"] + + libtiffs = [] + if Image.core.libtiff_support_custom_tags: + libtiffs.append(True) + libtiffs.append(False) + + for libtiff in libtiffs: + check_write(libtiff) + def test_multipage_compression(self): with Image.open("Tests/images/compression.tif") as im: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a18621d23..ee183ccba 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -553,9 +553,10 @@ class ImageFileDirectory_v2(MutableMapping): ) elif all(isinstance(v, float) for v in values): self.tagtype[tag] = TiffTags.DOUBLE - else: - if all(isinstance(v, str) for v in values): - self.tagtype[tag] = TiffTags.ASCII + elif all(isinstance(v, str) for v in values): + self.tagtype[tag] = TiffTags.ASCII + elif all(isinstance(v, bytes) for v in values): + self.tagtype[tag] = TiffTags.BYTE if self.tagtype[tag] == TiffTags.UNDEFINED: values = [ @@ -1548,16 +1549,17 @@ def _save(im, fp, filename): # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if ( - TiffTags.lookup(tag).type == TiffTags.UNDEFINED - or not Image.core.libtiff_support_custom_tags - ): + if not Image.core.libtiff_support_custom_tags: continue if tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] elif not (isinstance(value, (int, float, str, bytes))): continue + else: + type = TiffTags.lookup(tag).type + if type: + types[tag] = type if tag not in atts and tag not in blocklist: if isinstance(value, str): atts[tag] = value.encode("ascii", "replace") + b"\0" diff --git a/src/encode.c b/src/encode.c index 6506edb96..03a39448d 100644 --- a/src/encode.c +++ b/src/encode.c @@ -761,11 +761,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } } - if (PyBytes_Check(value) && type == TIFF_UNDEFINED) { - // For backwards compatibility - type = TIFF_ASCII; - } - if (PyTuple_Check(value)) { Py_ssize_t len; len = PyTuple_Size(value); @@ -797,7 +792,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } } - if (type == TIFF_BYTE) { + if (type == TIFF_BYTE || type == TIFF_UNDEFINED) { status = ImagingLibTiffSetField(&encoder->state, (ttag_t) key_int, PyBytes_Size(value), PyBytes_AsString(value)); From e219671be1dca941e7a68013fb20d1dc2c5a69f7 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Fri, 12 Jun 2020 00:34:40 +0300 Subject: [PATCH 18/34] Fix exception causes in PdfParser.py --- src/PIL/PdfParser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index fdb35eded..3c343c5e8 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -251,8 +251,8 @@ class PdfDict(collections.UserDict): def __getattr__(self, key): try: value = self[key.encode("us-ascii")] - except KeyError: - raise AttributeError(key) + except KeyError as e: + raise AttributeError(key) from e if isinstance(value, bytes): value = decode_text(value) if key.endswith("Date"): @@ -811,11 +811,11 @@ class PdfParser: if m: try: stream_len = int(result[b"Length"]) - except (TypeError, KeyError, ValueError): + except (TypeError, KeyError, ValueError) as e: raise PdfFormatError( "bad or missing Length in stream dict (%r)" % result.get(b"Length", None) - ) + ) from e stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) check_format_condition(m, "stream end not found") From 448cc46b1ea81711ab0508e6e95574b5b3f57ea7 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 16 Jun 2020 03:21:38 +0200 Subject: [PATCH 19/34] fix tcl tests --- .github/workflows/test-windows.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 7ae26b883..83dc5748b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -52,6 +52,11 @@ jobs: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} + - name: Set up TCL + if: "contains(matrix.python-version, 'pypy')" + run: Write-Host "::set-env name=TCL_LIBRARY::$env:pythonLocation\tcl\tcl8.5" + shell: pwsh + - name: Print build system information run: python .github/workflows/system-info.py From 18e974ae6f3ce36b7e27e2d4891bab08e57e6a0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Jun 2020 07:54:00 +1000 Subject: [PATCH 20/34] Updated lcms2 to 2.11 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1b5f2e056..e46bdf56c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -156,7 +156,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.9**. + above uses liblcms2. Tested with **1.19** and **2.7-2.11**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0ba8a135c..5bde823ca 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -195,9 +195,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_MIRROR + "/project/lcms/lcms/2.10/lcms2-2.10.tar.gz", - "filename": "lcms2-2.10.tar.gz", - "dir": "lcms2-2.10", + "url": SF_MIRROR + "/project/lcms/lcms/2.11/lcms2-2.11.tar.gz", + "filename": "lcms2-2.11.tar.gz", + "dir": "lcms2-2.11", "patch": { r"Projects\VC2017\lcms2_static\lcms2_static.vcxproj": { # default is /MD for x86 and /MT for x64, we need /MD always From 6ad98ba3c0c371c9bddf53b0939e7632ede8c2b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 Jun 2020 21:40:38 +1000 Subject: [PATCH 21/34] Do not ignore viewer if order is zero when registering --- Tests/test_imageshow.py | 12 +++++++----- src/PIL/ImageShow.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 64f15326b..fddc73bd1 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -17,19 +17,21 @@ def test_register(): ImageShow._viewers.pop() -def test_viewer_show(): +@pytest.mark.parametrize( + "order", [-1, 0], +) +def test_viewer_show(order): class TestViewer(ImageShow.Viewer): - methodCalled = False - def show_image(self, image, **options): self.methodCalled = True return True viewer = TestViewer() - ImageShow.register(viewer, -1) + ImageShow.register(viewer, order) for mode in ("1", "I;16", "LA", "RGB", "RGBA"): - with hopper() as im: + viewer.methodCalled = False + with hopper(mode) as im: assert ImageShow.show(im) assert viewer.methodCalled diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index fc5089423..cd85e81b4 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -31,7 +31,7 @@ def register(viewer, order=1): pass # raised if viewer wasn't a class if order > 0: _viewers.append(viewer) - elif order < 0: + else: _viewers.insert(0, viewer) From 84b8776bfcc85e671d7ce5eaccec12e833379b4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jun 2020 08:29:35 +1000 Subject: [PATCH 22/34] Updated libjpeg-turbo to 2.0.4 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 0ba8a135c..357d7dcb3 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,9 +105,9 @@ header = [ # dependencies, listed in order of compilation deps = { "libjpeg": { - "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.3/libjpeg-turbo-2.0.3.tar.gz", - "filename": "libjpeg-turbo-2.0.3.tar.gz", - "dir": "libjpeg-turbo-2.0.3", + "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4.tar.gz", + "filename": "libjpeg-turbo-2.0.4.tar.gz", + "dir": "libjpeg-turbo-2.0.4", "build": [ cmd_cmake( [ From 8a51ad07fdcaac054b1d95077a2c29f85e923f1d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 20 Jun 2020 22:41:04 +1000 Subject: [PATCH 23/34] Renamed variable Co-authored-by: Hugo van Kemenade --- src/PIL/TiffImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 43a2f7c40..fe9cc5a18 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1508,10 +1508,10 @@ def _save(im, fp, filename): # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) ifd[ROWSPERSTRIP] = im.size[1] - stripByteCounts = stride * im.size[1] - if stripByteCounts >= 2 ** 16: + strip_byte_counts = stride * im.size[1] + if strip_byte_counts >= 2 ** 16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG - ifd[STRIPBYTECOUNTS] = stripByteCounts + ifd[STRIPBYTECOUNTS] = strip_byte_counts ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer # no compression by default: ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) From 34d77a757863efe62b2c226305ec75f467f6d207 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 10:46:27 +1000 Subject: [PATCH 24/34] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 566e055a4..b3b96bb9e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,24 @@ Changelog (Pillow) 7.2.0 (unreleased) ------------------ +- Change STRIPBYTECOUNTS to LONG if necessary when saving #4626 + [radarhere, hugovk] + +- Write JFIF header when saving JPEG #4639 + [radarhere] + +- Replaced tiff_jpeg with jpeg compression when saving TIFF images #4627 + [radarhere] + +- Writing TIFF tags: improved BYTE, added UNDEFINED #4605 + [radarhere] + +- Consider transparency when pasting text on an RGBA image #4566 + [radarhere] + +- Added method argument to single frame WebP saving #4547 + [radarhere] + - Use ImageFileDirectory_v2 in Image.Exif #4637 [radarhere] From c82483e35a37919df9700485aa752e8c5a38f28c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 17:03:15 +1000 Subject: [PATCH 25/34] Install NumPy with OpenBLAS --- .github/workflows/macos-install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 6cd9dadf3..76a3ef2b7 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,7 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas PYTHONOPTIMIZE=0 pip install cffi pip install coverage @@ -11,6 +11,8 @@ pip install -U pytest pip install -U pytest-cov pip install pyroma pip install test-image-results + +echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg pip install numpy # extra test images From f7e47dffc4cd26ae7a37a8a8d2c387378cf746a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 11:01:30 +1000 Subject: [PATCH 26/34] Added release notes for #4605 [ci skip] --- docs/releasenotes/7.2.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 904e9d5ab..00baca474 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -27,3 +27,9 @@ Moved from the legacy :py:class:`PIL.TiffImagePlugin.ImageFileDirectory_v1` to :py:class:`PIL.Image.Exif`. This means that Exif RATIONALs and SIGNED_RATIONALs are now read as :py:class:`PIL.TiffImagePlugin.IFDRational`, instead of as a tuple with a numerator and a denominator. + +TIFF BYTE tags format +^^^^^^^^^^^^^^^^^^^^^ + +TIFF BYTE tags were previously read as a tuple containing a bytestring. They +are now read as just a single bytestring. From a324f4a466ab12d4c8cf0b95abe4a5bd79b1c7aa Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 12 Oct 2019 14:29:10 +0100 Subject: [PATCH 27/34] add version to features info block --- Tests/test_file_icns.py | 4 +-- Tests/test_file_jpeg.py | 4 +-- Tests/test_file_jpeg2k.py | 4 +-- Tests/test_file_png.py | 4 +-- Tests/test_imagecms.py | 4 +-- Tests/test_imagefont.py | 12 +++---- src/PIL/IcnsImagePlugin.py | 4 +-- src/PIL/features.py | 64 ++++++++++++++++++++++++++++++++------ 8 files changed, 73 insertions(+), 27 deletions(-) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index aeb146f7e..7bf7b72ec 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -2,14 +2,14 @@ import io import sys import pytest -from PIL import IcnsImagePlugin, Image +from PIL import IcnsImagePlugin, Image, features from .helper import assert_image_equal, assert_image_similar # sample icon file TEST_FILE = "Tests/images/pillow.icns" -ENABLE_JPEG2K = hasattr(Image.core, "jp2klib_version") +ENABLE_JPEG2K = features.check_codec("jpg_2000") def test_sanity(): diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ee0543027..be8e21d5a 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -3,7 +3,7 @@ import re from io import BytesIO import pytest -from PIL import ExifTags, Image, ImageFile, JpegImagePlugin +from PIL import ExifTags, Image, ImageFile, JpegImagePlugin, features from .helper import ( assert_image, @@ -41,7 +41,7 @@ class TestFileJpeg: def test_sanity(self): # internal version number - assert re.search(r"\d+\.\d+$", Image.core.jpeglib_version) + assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 7b8b7a04a..07f8e8e05 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -2,7 +2,7 @@ import re from io import BytesIO import pytest -from PIL import Image, ImageFile, Jpeg2KImagePlugin +from PIL import Image, ImageFile, Jpeg2KImagePlugin, features from .helper import ( assert_image_equal, @@ -35,7 +35,7 @@ def roundtrip(im, **options): def test_sanity(): # Internal version number - assert re.search(r"\d+\.\d+\.\d+$", Image.core.jp2klib_version) + assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index a44bdecf8..9bd8507d9 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -3,7 +3,7 @@ import zlib from io import BytesIO import pytest -from PIL import Image, ImageFile, PngImagePlugin +from PIL import Image, ImageFile, PngImagePlugin, features from .helper import ( PillowLeakTestCase, @@ -73,7 +73,7 @@ class TestFilePng: def test_sanity(self, tmp_path): # internal version number - assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", Image.core.zlib_version) + assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) test_file = str(tmp_path / "temp.png") diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 921fdc369..953731215 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -4,7 +4,7 @@ import re from io import BytesIO import pytest -from PIL import Image, ImageMode +from PIL import Image, ImageMode, features from .helper import assert_image, assert_image_equal, assert_image_similar, hopper @@ -46,7 +46,7 @@ def test_sanity(): assert list(map(type, v)) == [str, str, str, str] # internal version number - assert re.search(r"\d+\.\d+$", ImageCms.core.littlecms_version) + assert re.search(r"\d+\.\d+$", features.version_module("littlecms2")) skip_missing() i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index b62fc2e23..9602a3099 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,7 +7,7 @@ import sys from io import BytesIO import pytest -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, features from .helper import ( assert_image_equal, @@ -40,7 +40,7 @@ class TestImageFont: @classmethod def setup_class(self): - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + freetype = distutils.version.StrictVersion(features.version_module("freetype2")) self.metrics = self.METRICS["Default"] for conditions, metrics in self.METRICS.items(): @@ -67,7 +67,7 @@ class TestImageFont: ) def test_sanity(self): - assert re.search(r"\d+\.\d+\.\d+$", ImageFont.core.freetype2_version) + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) def test_font_properties(self): ttf = self.get_font() @@ -619,7 +619,7 @@ class TestImageFont: def test_variation_get(self): font = self.get_font() - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + freetype = distutils.version.StrictVersion(features.version_module("freetype2")) if freetype < "2.9.1": with pytest.raises(NotImplementedError): font.get_variation_names() @@ -691,7 +691,7 @@ class TestImageFont: def test_variation_set_by_name(self): font = self.get_font() - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + freetype = distutils.version.StrictVersion(features.version_module("freetype2")) if freetype < "2.9.1": with pytest.raises(NotImplementedError): font.set_variation_by_name("Bold") @@ -715,7 +715,7 @@ class TestImageFont: def test_variation_set_by_axes(self): font = self.get_font() - freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + freetype = distutils.version.StrictVersion(features.version_module("freetype2")) if freetype < "2.9.1": with pytest.raises(NotImplementedError): font.set_variation_by_axes([100]) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c00392615..9de7d8dfe 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -23,10 +23,10 @@ import subprocess import sys import tempfile -from PIL import Image, ImageFile, PngImagePlugin +from PIL import Image, ImageFile, PngImagePlugin, features from PIL._binary import i8 -enable_jpeg2k = hasattr(Image.core, "jp2klib_version") +enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: from PIL import Jpeg2KImagePlugin diff --git a/src/PIL/features.py b/src/PIL/features.py index 33e89cf24..e1823537e 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -8,11 +8,11 @@ import PIL from . import Image modules = { - "pil": "PIL._imaging", - "tkinter": "PIL._tkinter_finder", - "freetype2": "PIL._imagingft", - "littlecms2": "PIL._imagingcms", - "webp": "PIL._webp", + "pil": ("PIL._imaging", None), + "tkinter": ("PIL._tkinter_finder", None), + "freetype2": ("PIL._imagingft", "freetype2"), + "littlecms2": ("PIL._imagingcms", "littlecms"), + "webp": ("PIL._webp", None), } @@ -27,7 +27,7 @@ def check_module(feature): if not (feature in modules): raise ValueError("Unknown module %s" % feature) - module = modules[feature] + module, lib = modules[feature] try: __import__(module) @@ -36,6 +36,20 @@ def check_module(feature): return False +def version_module(feature): + if not check_module(feature): + return None + + module, lib = modules[feature] + + if lib is None: + return None + + attr = lib + "_version" + + return getattr(__import__(module, fromlist=[attr]), attr) + + def get_supported_modules(): """ :returns: A list of all supported modules. @@ -43,7 +57,12 @@ def get_supported_modules(): return [f for f in modules if check_module(f)] -codecs = {"jpg": "jpeg", "jpg_2000": "jpeg2k", "zlib": "zip", "libtiff": "libtiff"} +codecs = { + "jpg": ("jpeg", "jpeglib"), + "jpg_2000": ("jpeg2k", "jp2klib"), + "zlib": ("zip", "zlib"), + "libtiff": ("libtiff", "libtiff"), +} def check_codec(feature): @@ -57,11 +76,25 @@ def check_codec(feature): if feature not in codecs: raise ValueError("Unknown codec %s" % feature) - codec = codecs[feature] + codec, lib = codecs[feature] return codec + "_encoder" in dir(Image.core) +def version_codec(feature): + if not check_codec(feature): + return None + + codec, lib = codecs[feature] + + version = getattr(Image.core, lib + "_version") + + if feature == "libtiff": + return version.split("\n")[0].split("Version ")[1] + + return version + + def get_supported_codecs(): """ :returns: A list of all supported codecs. @@ -125,6 +158,14 @@ def check(feature): return False +def version(feature): + if feature in modules: + return version_module(feature) + if feature in codecs: + return version_codec(feature) + return None + + def get_supported(): """ :returns: A list of all supported modules, features, and codecs. @@ -187,7 +228,12 @@ def pilinfo(out=None, supported_formats=True): ("xcb", "XCB (X protocol)"), ]: if check(name): - print("---", feature, "support ok", file=out) + v = version(name) + if v is not None: + support = "ok (version {})".format(v) + else: + support = "ok" + print("---", feature, "support", support, file=out) else: print("***", feature, "support not installed", file=out) print("-" * 68, file=out) From 6c1ff252d60954bb6ae8af767dae858afa159562 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 05:35:43 +0200 Subject: [PATCH 28/34] check run-time version numbers where available, add docs --- docs/reference/features.rst | 18 ++++--- src/PIL/features.py | 89 ++++++++++++++++++++++++---------- src/_imaging.c | 9 ++++ src/_imagingcms.c | 4 +- src/_imagingft.c | 7 +++ src/_webp.c | 13 +++++ src/libImaging/QuantPngQuant.c | 9 ++++ src/libImaging/ZipEncode.c | 2 +- 8 files changed, 118 insertions(+), 33 deletions(-) diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 196f938ed..47e9a6d63 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -8,6 +8,7 @@ The :py:mod:`PIL.features` module can be used to detect which Pillow features ar .. autofunction:: PIL.features.pilinfo .. autofunction:: PIL.features.check +.. autofunction:: PIL.features.version .. autofunction:: PIL.features.get_supported Modules @@ -16,28 +17,31 @@ Modules Support for the following modules can be checked: * ``pil``: The Pillow core module, required for all functionality. -* ``tkinter``: Tkinter support. +* ``tkinter``: Tkinter support. Version number not available. * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. * ``webp``: WebP image support. .. autofunction:: PIL.features.check_module +.. autofunction:: PIL.features.version_module .. autofunction:: PIL.features.get_supported_modules Codecs ------ -These are only checked during Pillow compilation. +Support for these is only checked during Pillow compilation. If the required library was uninstalled from the system, the ``pil`` core module may fail to load instead. +Except for ``jpg``, the version number is checked at run-time. Support for the following codecs can be checked: -* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats. +* ``jpg``: (compile time) Libjpeg support, required for JPEG based image formats. Only compile time version number is available. * ``jpg_2000``: (compile time) OpenJPEG support, required for JPEG 2000 image formats. * ``zlib``: (compile time) Zlib support, required for zlib compressed formats, such as PNG. * ``libtiff``: (compile time) LibTIFF support, required for TIFF based image formats. .. autofunction:: PIL.features.check_codec +.. autofunction:: PIL.features.version_codec .. autofunction:: PIL.features.get_supported_codecs Features @@ -45,16 +49,18 @@ Features Some of these are only checked during Pillow compilation. If the required library was uninstalled from the system, the relevant module may fail to load instead. +Feature version numbers are available only where stated. Support for the following features can be checked: -* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. +* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``transp_webp``: Support for transparency in WebP images. * ``webp_mux``: (compile time) Support for EXIF data in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. -* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. -* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. +* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. +* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. .. autofunction:: PIL.features.check_feature +.. autofunction:: PIL.features.version_feature .. autofunction:: PIL.features.get_supported_features diff --git a/src/PIL/features.py b/src/PIL/features.py index e1823537e..4f1bb0b8f 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -8,11 +8,11 @@ import PIL from . import Image modules = { - "pil": ("PIL._imaging", None), + "pil": ("PIL._imaging", "PILLOW_VERSION"), "tkinter": ("PIL._tkinter_finder", None), - "freetype2": ("PIL._imagingft", "freetype2"), - "littlecms2": ("PIL._imagingcms", "littlecms"), - "webp": ("PIL._webp", None), + "freetype2": ("PIL._imagingft", "freetype2_version"), + "littlecms2": ("PIL._imagingcms", "littlecms_version"), + "webp": ("PIL._webp", "webpdecoder_version"), } @@ -27,7 +27,7 @@ def check_module(feature): if not (feature in modules): raise ValueError("Unknown module %s" % feature) - module, lib = modules[feature] + module, ver = modules[feature] try: __import__(module) @@ -37,17 +37,21 @@ def check_module(feature): def version_module(feature): + """ + :param feature: The module to check for. + :returns: + The loaded version number as a string, or ``None`` if unknown or not available. + :raises ValueError: If the module is not defined in this version of Pillow. + """ if not check_module(feature): return None - module, lib = modules[feature] + module, ver = modules[feature] - if lib is None: + if ver is None: return None - attr = lib + "_version" - - return getattr(__import__(module, fromlist=[attr]), attr) + return getattr(__import__(module, fromlist=[ver]), ver) def get_supported_modules(): @@ -82,6 +86,13 @@ def check_codec(feature): def version_codec(feature): + """ + :param feature: The codec to check for. + :returns: + The version number as a string, or ``None`` if not available. + Checked at compile time for ``jpg``, run-time otherwise. + :raises ValueError: If the codec is not defined in this version of Pillow. + """ if not check_codec(feature): return None @@ -103,13 +114,13 @@ def get_supported_codecs(): features = { - "webp_anim": ("PIL._webp", "HAVE_WEBPANIM"), - "webp_mux": ("PIL._webp", "HAVE_WEBPMUX"), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), - "raqm": ("PIL._imagingft", "HAVE_RAQM"), - "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"), - "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"), - "xcb": ("PIL._imaging", "HAVE_XCB"), + "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), + "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), + "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), + "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), + "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), + "xcb": ("PIL._imaging", "HAVE_XCB", None), } @@ -124,7 +135,7 @@ def check_feature(feature): if feature not in features: raise ValueError("Unknown feature %s" % feature) - module, flag = features[feature] + module, flag, ver = features[feature] try: imported_module = __import__(module, fromlist=["PIL"]) @@ -133,6 +144,23 @@ def check_feature(feature): return None +def version_feature(feature): + """ + :param feature: The feature to check for. + :returns: The version number as a string, or ``None`` if not available. + :raises ValueError: If the feature is not defined in this version of Pillow. + """ + if not check_feature(feature): + return None + + module, flag, ver = features[feature] + + if ver is None: + return None + + return getattr(__import__(module, fromlist=[ver]), ver) + + def get_supported_features(): """ :returns: A list of all supported features. @@ -142,9 +170,9 @@ def get_supported_features(): def check(feature): """ - :param feature: A module, feature, or codec name. + :param feature: A module, codec, or feature name. :returns: - ``True`` if the module, feature, or codec is available, + ``True`` if the module, codec, or feature is available, ``False`` or ``None`` otherwise. """ @@ -159,10 +187,18 @@ def check(feature): def version(feature): + """ + :param feature: + The module, codec, or feature to check for. + :returns: + The version number as a string, or ``None`` if unknown or not available. + """ if feature in modules: return version_module(feature) if feature in codecs: return version_codec(feature) + if feature in features: + return version_feature(feature) return None @@ -228,12 +264,15 @@ def pilinfo(out=None, supported_formats=True): ("xcb", "XCB (X protocol)"), ]: if check(name): - v = version(name) - if v is not None: - support = "ok (version {})".format(v) + if name == "jpg" and check_feature("libjpeg_turbo"): + v = "libjpeg-turbo " + version_feature("libjpeg_turbo") else: - support = "ok" - print("---", feature, "support", support, file=out) + v = version(name) + if v is not None: + t = "compiled for" if name in ("pil", "jpg") else "loaded" + print("---", feature, "support ok,", t, "version", v, file=out) + else: + print("---", feature, "support ok", file=out) else: print("***", feature, "support not installed", file=out) print("-" * 68, file=out) diff --git a/src/_imaging.c b/src/_imaging.c index 40bfbf2fe..1ed5e8a42 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4168,12 +4168,21 @@ setup_module(PyObject* m) { #ifdef LIBJPEG_TURBO_VERSION PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_True); + #define tostr1(a) #a + #define tostr(a) tostr1(a) + PyDict_SetItemString(d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION))); + #undef tostr + #undef tostr1 #else PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_False); #endif #ifdef HAVE_LIBIMAGEQUANT PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_True); + { + extern const char* ImagingImageQuantVersion(void); + PyDict_SetItemString(d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion())); + } #else PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_False); #endif diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 60b6b7228..7f23d5964 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1608,6 +1608,7 @@ static int setup_module(PyObject* m) { PyObject *d; PyObject *v; + int vn; d = PyModule_GetDict(m); @@ -1622,7 +1623,8 @@ setup_module(PyObject* m) { d = PyModule_GetDict(m); - v = PyUnicode_FromFormat("%d.%d", LCMS_VERSION / 100, LCMS_VERSION % 100); + vn = cmsGetEncodedCMMversion(); + v = PyUnicode_FromFormat("%d.%d", vn / 100, vn % 100); PyDict_SetItemString(d, "littlecms_version", v); return 0; diff --git a/src/_imagingft.c b/src/_imagingft.c index e0ff7521c..d7ff7ad28 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -81,6 +81,7 @@ typedef struct { static PyTypeObject Font_Type; +typedef const char* (*t_raqm_version_string) (void); typedef bool (*t_raqm_version_atleast)(unsigned int major, unsigned int minor, unsigned int micro); @@ -112,6 +113,7 @@ typedef void (*t_raqm_destroy) (raqm_t *rq); typedef struct { void* raqm; int version; + t_raqm_version_string version_string; t_raqm_version_atleast version_atleast; t_raqm_create create; t_raqm_set_text set_text; @@ -173,6 +175,7 @@ setraqm(void) } #ifndef _WIN32 + p_raqm.version_string = (t_raqm_version_atleast)dlsym(p_raqm.raqm, "raqm_version_string"); p_raqm.version_atleast = (t_raqm_version_atleast)dlsym(p_raqm.raqm, "raqm_version_atleast"); p_raqm.create = (t_raqm_create)dlsym(p_raqm.raqm, "raqm_create"); p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text"); @@ -206,6 +209,7 @@ setraqm(void) return 2; } #else + p_raqm.version_string = (t_raqm_version_atleast)GetProcAddress(p_raqm.raqm, "raqm_version_string"); p_raqm.version_atleast = (t_raqm_version_atleast)GetProcAddress(p_raqm.raqm, "raqm_version_atleast"); p_raqm.create = (t_raqm_create)GetProcAddress(p_raqm.raqm, "raqm_create"); p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text"); @@ -1251,6 +1255,9 @@ setup_module(PyObject* m) { setraqm(); v = PyBool_FromLong(!!p_raqm.raqm); PyDict_SetItemString(d, "HAVE_RAQM", v); + if (p_raqm.version_string) { + PyDict_SetItemString(d, "raqm_version", PyUnicode_FromString(p_raqm.version_string())); + } return 0; } diff --git a/src/_webp.c b/src/_webp.c index c2b363cd0..468a9ff73 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -821,6 +821,16 @@ PyObject* WebPDecoderVersion_wrapper() { return Py_BuildValue("i", WebPGetDecoderVersion()); } +// Version as string +const char* +WebPDecoderVersion_str(void) +{ + static char version[20]; + int version_number = WebPGetDecoderVersion(); + sprintf(version, "%d.%d.%d", version_number >> 16, (version_number >> 8) % 0x100, version_number % 0x100); + return version; +} + /* * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. * Files that are valid with 0.3 are reported as being invalid. @@ -872,10 +882,13 @@ void addTransparencyFlagToModule(PyObject* m) { } static int setup_module(PyObject* m) { + PyObject* d = PyModule_GetDict(m); addMuxFlagToModule(m); addAnimFlagToModule(m); addTransparencyFlagToModule(m); + PyDict_SetItemString(d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); + #ifdef HAVE_WEBPANIM /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || diff --git a/src/libImaging/QuantPngQuant.c b/src/libImaging/QuantPngQuant.c index 753ceb02f..7a23ec8c5 100644 --- a/src/libImaging/QuantPngQuant.c +++ b/src/libImaging/QuantPngQuant.c @@ -113,4 +113,13 @@ err: return result; } +const char* +ImagingImageQuantVersion(void) +{ + static char version[20]; + int number = liq_version(); + sprintf(version, "%d.%d.%d", number / 10000, (number / 100) % 100, number % 100); + return version; +} + #endif diff --git a/src/libImaging/ZipEncode.c b/src/libImaging/ZipEncode.c index 0b4435678..84ccb14ea 100644 --- a/src/libImaging/ZipEncode.c +++ b/src/libImaging/ZipEncode.c @@ -373,7 +373,7 @@ ImagingZipEncodeCleanup(ImagingCodecState state) { const char* ImagingZipVersion(void) { - return ZLIB_VERSION; + return zlibVersion(); } #endif From d5a6b2584e3e1a2dcc15c5adec291cd0390fb785 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 15 Jun 2020 15:32:30 +0200 Subject: [PATCH 29/34] add tests for version numbers --- Tests/test_features.py | 39 ++++++++++++++++++++++++++++++++++++++ Tests/test_file_libtiff.py | 6 +++++- Tests/test_file_webp.py | 4 +++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 7cfa08071..1e7692204 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,4 +1,5 @@ import io +import re import pytest from PIL import features @@ -21,6 +22,27 @@ def test_check(): assert features.check_feature(feature) == features.check(feature) +def test_version(): + # Check the correctness of the convenience function + # and the format of version numbers + + def test(name, function): + version = features.version(name) + if not features.check(name): + assert version is None + else: + assert function(name) == version + if name != "PIL": + assert version is None or re.search(r"\d+(\.\d+)*$", version) + + for module in features.modules: + test(module, features.version_module) + for codec in features.codecs: + test(codec, features.version_codec) + for feature in features.features: + test(feature, features.version_feature) + + @skip_unless_feature("webp") def test_webp_transparency(): assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() @@ -37,9 +59,22 @@ def test_webp_anim(): assert features.check("webp_anim") == _webp.HAVE_WEBPANIM +@skip_unless_feature("libjpeg_turbo") +def test_libjpeg_turbo_version(): + assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) + + +@skip_unless_feature("libimagequant") +def test_libimagequant_version(): + assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) + + def test_check_modules(): for feature in features.modules: assert features.check_module(feature) in [True, False] + + +def test_check_codecs(): for feature in features.codecs: assert features.check_codec(feature) in [True, False] @@ -64,6 +99,8 @@ def test_unsupported_codec(): # Act / Assert with pytest.raises(ValueError): features.check_codec(codec) + with pytest.raises(ValueError): + features.version_codec(codec) def test_unsupported_module(): @@ -72,6 +109,8 @@ def test_unsupported_module(): # Act / Assert with pytest.raises(ValueError): features.check_module(module) + with pytest.raises(ValueError): + features.version_module(module) def test_pilinfo(): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 9d9e49289..c30eb54eb 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -3,11 +3,12 @@ import io import itertools import logging import os +import re from collections import namedtuple from ctypes import c_float import pytest -from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags +from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features from .helper import ( assert_image_equal, @@ -47,6 +48,9 @@ class LibTiffTestCase: class TestFileLibTiff(LibTiffTestCase): + def test_version(self): + assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) + def test_g4_tiff(self, tmp_path): """Test the ordinary file path load path""" diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f538b0ecf..25a4bb8da 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,7 +1,8 @@ import io +import re import pytest -from PIL import Image, WebPImagePlugin +from PIL import Image, WebPImagePlugin, features from .helper import ( assert_image_similar, @@ -38,6 +39,7 @@ class TestFileWebp: def test_version(self): _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() + assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) def test_read_rgb(self): """ From 659ce90af1b67151ad5089798bc2252239c345c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 19:09:09 +1000 Subject: [PATCH 30/34] Fixed typo [ci skip] --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 203921c0b..885fba4cd 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -18,7 +18,7 @@ ImageFile.raise_ioerror .. deprecated:: 7.2.0 ``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` -is now deprecated and will be removed in a future released. Use +is now deprecated and will be removed in a future release. Use ``ImageFile.raise_oserror`` instead. PILLOW_VERSION constant From c76dfbaef597de34baefb9d16d0a174e2877b73e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 19:11:09 +1000 Subject: [PATCH 31/34] Added release notes for #4536 [ci skip] --- docs/releasenotes/7.2.0.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 00baca474..26a1464a4 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -33,3 +33,13 @@ TIFF BYTE tags format TIFF BYTE tags were previously read as a tuple containing a bytestring. They are now read as just a single bytestring. + +Deprecations +^^^^^^^^^^^^ + +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ + +``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` +is now deprecated and will be removed in a future release. Use +``ImageFile.raise_oserror`` instead. From d4f490183838a03762521e1f8e4063dc4481f08a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 19:29:33 +1000 Subject: [PATCH 32/34] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3b96bb9e..96239a6e2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -62,6 +62,9 @@ Changelog (Pillow) - Fix pickling WebP #4561 [hugovk, radarhere] +- Replace IOError and WindowsError aliases with OSError #4536 + [hugovk, radarhere] + 7.1.2 (2020-04-25) ------------------ From 34ba2ae139d46113ab7b1c683f3a6f6d5a8f89c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 20:26:10 +1000 Subject: [PATCH 33/34] Removed comments suggesting users override functions --- src/PIL/Image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9d94bce0e..2fc37d9ce 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3139,11 +3139,10 @@ def register_encoder(name, encoder): # -------------------------------------------------------------------- -# Simple display support. User code may override this. +# Simple display support. def _show(image, **options): - # override me, as necessary _showxv(image, **options) From 24672a2f7502e0ae0534bd0aadf17b2c3e200787 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 21 Jun 2020 18:07:10 +0100 Subject: [PATCH 34/34] simplify output Co-authored-by: Hugo van Kemenade --- src/PIL/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 4f1bb0b8f..66b093350 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -270,7 +270,7 @@ def pilinfo(out=None, supported_formats=True): v = version(name) if v is not None: t = "compiled for" if name in ("pil", "jpg") else "loaded" - print("---", feature, "support ok,", t, "version", v, file=out) + print("---", feature, "support ok,", t, v, file=out) else: print("---", feature, "support ok", file=out) else: