From 61753891869d75bff3ce9256b684d3da0762f01a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Jan 2021 12:45:02 +1100 Subject: [PATCH 01/23] Only read different sizes for "Large Thumbnail" frames --- Tests/images/ignore_frame_size.mpo | Bin 0 -> 4405 bytes Tests/test_file_mpo.py | 13 ++++++++++++- src/PIL/MpoImagePlugin.py | 8 +++++--- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 Tests/images/ignore_frame_size.mpo diff --git a/Tests/images/ignore_frame_size.mpo b/Tests/images/ignore_frame_size.mpo new file mode 100644 index 0000000000000000000000000000000000000000..c4d60707a47f18ffe644a27443b974b3c3c613f2 GIT binary patch literal 4405 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 h33pE!X2e@Fx7){P#9i~X->y}7kfIL?53 Date: Fri, 1 Jan 2021 13:00:01 +1100 Subject: [PATCH 02/23] Changed MP Type to match #1631 image --- Tests/images/sugarshack_frame_size.mpo | Bin 120198 -> 120198 bytes Tests/test_file_mpo.py | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo index 81d58e64b8268eac4210f1506b4dbded0b92ffda..009280a79a648be15d6064c96f931e71ce9fde8e 100644 GIT binary patch delta 22 ecmZo$&EB?}eZx_4W=00a<`d%EPlz+_+Y10*> Date: Thu, 21 Jan 2021 19:29:11 +1100 Subject: [PATCH 03/23] Handle PCX images with an odd stride --- Tests/images/odd_stride.pcx | Bin 0 -> 14313 bytes Tests/test_file_pcx.py | 8 ++++++++ src/PIL/PcxImagePlugin.py | 13 +++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 Tests/images/odd_stride.pcx diff --git a/Tests/images/odd_stride.pcx b/Tests/images/odd_stride.pcx new file mode 100644 index 0000000000000000000000000000000000000000..ee0c2eecaebe5a629ccfab523971c347a878ae79 GIT binary patch literal 14313 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 literal 0 HcmV?d00001 diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 670c03b95..61e33a57b 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -44,6 +44,14 @@ def test_odd(tmp_path): _roundtrip(tmp_path, hopper(mode).resize((511, 511))) +def test_odd_read(): + # Reading an image with an odd stride, making it malformed + with Image.open("Tests/images/odd_stride.pcx") as im: + im.load() + + assert im.size == (371, 150) + + def test_pil184(): # Check reading of files where xmin/xmax is not zero. diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3874e5436..d2e166bdd 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -66,13 +66,13 @@ class PcxImageFile(ImageFile.ImageFile): version = s[1] bits = s[3] planes = s[65] - ignored_stride = i16(s, 66) + provided_stride = i16(s, 66) logger.debug( "PCX version %s, bits %s, planes %s, stride %s", version, bits, planes, - ignored_stride, + provided_stride, ) self.info["dpi"] = i16(s, 12), i16(s, 14) @@ -110,10 +110,15 @@ class PcxImageFile(ImageFile.ImageFile): self.mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] - # don't trust the passed in stride. Calculate for ourselves. + # Don't trust the passed in stride. + # Calculate the approximate position for ourselves. # CVE-2020-35653 stride = (self._size[0] * bits + 7) // 8 - stride += stride % 2 + + # While the specification states that this must be even, + # not all images follow this + if provided_stride != stride: + stride += stride % 2 bbox = (0, 0) + self.size logger.debug("size: %sx%s", *self.size) From b39977e1c2fca2aef8c28d31522136a11a9be59e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Jan 2021 21:33:35 +1100 Subject: [PATCH 04/23] Document license for several fonts --- .../DejaVuSans-24-1-stripped.ttf | Bin .../DejaVuSans-24-2-stripped.ttf | Bin .../DejaVuSans-24-4-stripped.ttf | Bin .../DejaVuSans-24-8-stripped.ttf | Bin Tests/fonts/{ => DejaVuSans}/DejaVuSans.ttf | Bin Tests/fonts/DejaVuSans/LICENSE.txt | 40 ++++++++++++++++++ Tests/fonts/LICENSE.txt | 3 +- Tests/test_imagefont.py | 12 +++--- Tests/test_imagefontctl.py | 2 +- 9 files changed, 50 insertions(+), 7 deletions(-) rename Tests/fonts/{ => DejaVuSans}/DejaVuSans-24-1-stripped.ttf (100%) rename Tests/fonts/{ => DejaVuSans}/DejaVuSans-24-2-stripped.ttf (100%) rename Tests/fonts/{ => DejaVuSans}/DejaVuSans-24-4-stripped.ttf (100%) rename Tests/fonts/{ => DejaVuSans}/DejaVuSans-24-8-stripped.ttf (100%) rename Tests/fonts/{ => DejaVuSans}/DejaVuSans.ttf (100%) create mode 100644 Tests/fonts/DejaVuSans/LICENSE.txt diff --git a/Tests/fonts/DejaVuSans-24-1-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-1-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-1-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-1-stripped.ttf diff --git a/Tests/fonts/DejaVuSans-24-2-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-2-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-2-stripped.ttf diff --git a/Tests/fonts/DejaVuSans-24-4-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-4-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-4-stripped.ttf diff --git a/Tests/fonts/DejaVuSans-24-8-stripped.ttf b/Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf similarity index 100% rename from Tests/fonts/DejaVuSans-24-8-stripped.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf diff --git a/Tests/fonts/DejaVuSans.ttf b/Tests/fonts/DejaVuSans/DejaVuSans.ttf similarity index 100% rename from Tests/fonts/DejaVuSans.ttf rename to Tests/fonts/DejaVuSans/DejaVuSans.ttf diff --git a/Tests/fonts/DejaVuSans/LICENSE.txt b/Tests/fonts/DejaVuSans/LICENSE.txt new file mode 100644 index 000000000..30516578f --- /dev/null +++ b/Tests/fonts/DejaVuSans/LICENSE.txt @@ -0,0 +1,40 @@ +DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. + +DejaVu Fonts — License +Fonts are © Bitstream (see below). DejaVu changes are in public domain. Explanation of copyright is on Gnome page on Bitstream Vera fonts. Glyphs imported from Arev fonts are © Tavmjung Bah (see below) + +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. + +Arev Fonts Copyright +Original text + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the modifications to the Bitstream Vera Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr. \ No newline at end of file diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 06eaa9a4e..88a28de59 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -15,8 +15,9 @@ FreeMono.ttf is licensed under GPLv3, with the GPL font exception. OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) -DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. +KhmerOSBattambang-Regular.ttf is licensed under LGPL-2.1 or later. +FreeMono.ttf is licensed under GPLv3. 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 0c219fed1..2a2349e3b 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -52,7 +52,7 @@ class TestImageFont: ttf_copy = ttf.font_variant(size=FONT_SIZE + 1) assert ttf_copy.size == FONT_SIZE + 1 - second_font_path = "Tests/fonts/DejaVuSans.ttf" + second_font_path = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" ttf_copy = ttf.font_variant(font=second_font_path) assert ttf_copy.path == second_font_path @@ -156,8 +156,8 @@ class TestImageFont: ("text", "L", "FreeMono.ttf", 15, 36, 36), ("text", "1", "FreeMono.ttf", 15, 36, 36), # issue 4177 - ("rrr", "L", "DejaVuSans.ttf", 18, 21, 22.21875), - ("rrr", "1", "DejaVuSans.ttf", 18, 24, 22.21875), + ("rrr", "L", "DejaVuSans/DejaVuSans.ttf", 18, 21, 22.21875), + ("rrr", "1", "DejaVuSans/DejaVuSans.ttf", 18, 24, 22.21875), # test 'l' not including extra margin # using exact value 2047 / 64 for raqm, checked with debugger ("ill", "L", "OpenSansCondensed-LightItalic.ttf", 63, 33, 31.984375), @@ -855,7 +855,7 @@ class TestImageFont: layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" font = ImageFont.truetype( - f"Tests/fonts/DejaVuSans-24-{bpp}-stripped.ttf", + f"Tests/fonts/DejaVuSans/DejaVuSans-24-{bpp}-stripped.ttf", 24, layout_engine=self.LAYOUT_ENGINE, ) @@ -963,7 +963,9 @@ def test_render_mono_size(): im = Image.new("P", (100, 30), "white") draw = ImageDraw.Draw(im) ttf = ImageFont.truetype( - "Tests/fonts/DejaVuSans.ttf", 18, layout_engine=ImageFont.LAYOUT_BASIC + "Tests/fonts/DejaVuSans/DejaVuSans.ttf", + 18, + layout_engine=ImageFont.LAYOUT_BASIC, ) draw.text((10, 10), "r" * 10, "black", ttf) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 82e2b4ebc..a80aca2fb 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -10,7 +10,7 @@ from .helper import ( ) FONT_SIZE = 20 -FONT_PATH = "Tests/fonts/DejaVuSans.ttf" +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" pytestmark = skip_unless_feature("raqm") From f2f92d22d180ddcecab5bf9c0a4c0151c04d0980 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Jan 2021 21:10:49 +1100 Subject: [PATCH 05/23] Do not use "use built-in mapper WIN32 only" --- src/PIL/ImageFile.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f2a55cb54..f58de95bd 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -192,24 +192,14 @@ class ImageFile(Image.Image): and args[0] in Image._MAPMODES ): try: - if hasattr(Image.core, "map"): - # use built-in mapper WIN32 only - self.map = Image.core.map(self.filename) - self.map.seek(offset) - self.im = self.map.readimage( - self.mode, self.size, args[1], args[2] - ) - else: - # use mmap, if possible - import mmap + # use mmap, if possible + import mmap - with open(self.filename) as fp: - self.map = mmap.mmap( - fp.fileno(), 0, access=mmap.ACCESS_READ - ) - self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, offset, args - ) + with open(self.filename) as fp: + self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) + self.im = Image.core.map_buffer( + self.map, self.size, decoder_name, offset, args + ) readonly = 1 # After trashing self.im, # we might need to reload the palette data. From 685e95118250e764ff50e6ed29d5ed96fc873b4a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 25 Jan 2021 21:13:07 +1100 Subject: [PATCH 06/23] Removed unused C code --- src/_imaging.c | 5 - src/map.c | 260 ------------------------------------------------- 2 files changed, 265 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 01dd22486..a5b12d325 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3973,8 +3973,6 @@ PyPath_Create(ImagingObject *self, PyObject *args); extern PyObject * PyOutline_Create(ImagingObject *self, PyObject *args); -extern PyObject * -PyImaging_Mapper(PyObject *self, PyObject *args); extern PyObject * PyImaging_MapBuffer(PyObject *self, PyObject *args); @@ -4030,9 +4028,6 @@ static PyMethodDef functions[] = { /* Memory mapping */ #ifdef WITH_MAPPING -#ifdef _WIN32 - {"map", (PyCFunction)PyImaging_Mapper, 1}, -#endif {"map_buffer", (PyCFunction)PyImaging_MapBuffer, 1}, #endif diff --git a/src/map.c b/src/map.c index 2636a684b..c298bd148 100644 --- a/src/map.c +++ b/src/map.c @@ -28,269 +28,9 @@ PyImaging_CheckBuffer(PyObject *buffer); extern int PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view); -/* -------------------------------------------------------------------- */ -/* Standard mapper */ - -typedef struct { - PyObject_HEAD char *base; - int size; - int offset; -#ifdef _WIN32 - HANDLE hFile; - HANDLE hMap; -#endif -} ImagingMapperObject; - -static PyTypeObject ImagingMapperType; - -ImagingMapperObject * -PyImaging_MapperNew(const char *filename, int readonly) { - ImagingMapperObject *mapper; - - if (PyType_Ready(&ImagingMapperType) < 0) { - return NULL; - } - - mapper = PyObject_New(ImagingMapperObject, &ImagingMapperType); - if (mapper == NULL) { - return NULL; - } - - mapper->base = NULL; - mapper->size = mapper->offset = 0; - -#ifdef _WIN32 - mapper->hFile = (HANDLE)-1; - mapper->hMap = (HANDLE)-1; - - /* FIXME: currently supports readonly mappings only */ - mapper->hFile = CreateFile( - filename, - GENERIC_READ, - FILE_SHARE_READ, - NULL, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, - NULL); - if (mapper->hFile == (HANDLE)-1) { - PyErr_SetString(PyExc_OSError, "cannot open file"); - Py_DECREF(mapper); - return NULL; - } - - mapper->hMap = CreateFileMapping(mapper->hFile, NULL, PAGE_READONLY, 0, 0, NULL); - if (mapper->hMap == (HANDLE)-1) { - CloseHandle(mapper->hFile); - PyErr_SetString(PyExc_OSError, "cannot map file"); - Py_DECREF(mapper); - return NULL; - } - - mapper->base = (char *)MapViewOfFile(mapper->hMap, FILE_MAP_READ, 0, 0, 0); - - mapper->size = GetFileSize(mapper->hFile, 0); -#endif - - return mapper; -} - -static void -mapping_dealloc(ImagingMapperObject *mapper) { -#ifdef _WIN32 - if (mapper->base != 0) { - UnmapViewOfFile(mapper->base); - } - if (mapper->hMap != (HANDLE)-1) { - CloseHandle(mapper->hMap); - } - if (mapper->hFile != (HANDLE)-1) { - CloseHandle(mapper->hFile); - } - mapper->base = 0; - mapper->hMap = mapper->hFile = (HANDLE)-1; -#endif - PyObject_Del(mapper); -} - -/* -------------------------------------------------------------------- */ -/* standard file operations */ - -static PyObject * -mapping_read(ImagingMapperObject *mapper, PyObject *args) { - PyObject *buf; - - int size = -1; - if (!PyArg_ParseTuple(args, "|i", &size)) { - return NULL; - } - - /* check size */ - if (size < 0 || mapper->offset + size > mapper->size) { - size = mapper->size - mapper->offset; - } - if (size < 0) { - size = 0; - } - - buf = PyBytes_FromStringAndSize(NULL, size); - if (!buf) { - return NULL; - } - - if (size > 0) { - memcpy(PyBytes_AsString(buf), mapper->base + mapper->offset, size); - mapper->offset += size; - } - - return buf; -} - -static PyObject * -mapping_seek(ImagingMapperObject *mapper, PyObject *args) { - int offset; - int whence = 0; - if (!PyArg_ParseTuple(args, "i|i", &offset, &whence)) { - return NULL; - } - - switch (whence) { - case 0: /* SEEK_SET */ - mapper->offset = offset; - break; - case 1: /* SEEK_CUR */ - mapper->offset += offset; - break; - case 2: /* SEEK_END */ - mapper->offset = mapper->size + offset; - break; - default: - /* FIXME: raise ValueError? */ - break; - } - - Py_INCREF(Py_None); - return Py_None; -} - -/* -------------------------------------------------------------------- */ -/* map entire image */ - extern PyObject * PyImagingNew(Imaging im); -static void -ImagingDestroyMap(Imaging im) { - return; /* nothing to do! */ -} - -static PyObject * -mapping_readimage(ImagingMapperObject *mapper, PyObject *args) { - int y, size; - Imaging im; - - char *mode; - int xsize; - int ysize; - int stride; - int orientation; - if (!PyArg_ParseTuple( - args, "s(ii)ii", &mode, &xsize, &ysize, &stride, &orientation)) { - return NULL; - } - - if (stride <= 0) { - /* FIXME: maybe we should call ImagingNewPrologue instead */ - if (!strcmp(mode, "L") || !strcmp(mode, "P")) { - stride = xsize; - } else if (!strcmp(mode, "I;16") || !strcmp(mode, "I;16B")) { - stride = xsize * 2; - } else { - stride = xsize * 4; - } - } - - size = ysize * stride; - - if (mapper->offset + size > mapper->size) { - PyErr_SetString(PyExc_OSError, "image file truncated"); - return NULL; - } - - im = ImagingNewPrologue(mode, xsize, ysize); - if (!im) { - return NULL; - } - - /* setup file pointers */ - if (orientation > 0) { - for (y = 0; y < ysize; y++) { - im->image[y] = mapper->base + mapper->offset + y * stride; - } - } else { - for (y = 0; y < ysize; y++) { - im->image[ysize - y - 1] = mapper->base + mapper->offset + y * stride; - } - } - - im->destroy = ImagingDestroyMap; - - mapper->offset += size; - - return PyImagingNew(im); -} - -static struct PyMethodDef methods[] = { - /* standard file interface */ - {"read", (PyCFunction)mapping_read, 1}, - {"seek", (PyCFunction)mapping_seek, 1}, - /* extensions */ - {"readimage", (PyCFunction)mapping_readimage, 1}, - {NULL, NULL} /* sentinel */ -}; - -static PyTypeObject ImagingMapperType = { - PyVarObject_HEAD_INIT(NULL, 0) "ImagingMapper", /*tp_name*/ - sizeof(ImagingMapperObject), /*tp_size*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)mapping_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ -}; - -PyObject * -PyImaging_Mapper(PyObject *self, PyObject *args) { - char *filename; - if (!PyArg_ParseTuple(args, "s", &filename)) { - return NULL; - } - - return (PyObject *)PyImaging_MapperNew(filename, 1); -} - /* -------------------------------------------------------------------- */ /* Buffer mapper */ From e4b9f88de4378ae622b54998b186e93e68fab4c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Jan 2021 12:59:45 +1100 Subject: [PATCH 07/23] Updated test now that Win32 uses map_buffer --- Tests/test_map.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Tests/test_map.py b/Tests/test_map.py index 2b65fb3f9..9131e6b7d 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -4,10 +4,6 @@ import pytest from PIL import Image -from .helper import is_win32 - -pytestmark = pytest.mark.skipif(is_win32(), reason="Win32 does not call map_buffer") - def test_overflow(): # There is the potential to overflow comparisons in map.c From 11cb3fba9c93275d78f4339973560938fc07bd9e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 30 Jan 2021 13:01:42 +1100 Subject: [PATCH 08/23] Added test --- Tests/test_map.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/test_map.py b/Tests/test_map.py index 9131e6b7d..752c5f268 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -23,6 +23,13 @@ def test_overflow(): Image.MAX_IMAGE_PIXELS = max_pixels +def test_tobytes(): + # Previously raised an access violation on Windows + with Image.open("Tests/images/l2rgb_read.bmp") as im: + with pytest.raises((ValueError, MemoryError, OSError)): + im.tobytes() + + @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") From c0ee869c2c09bca7825e0fd9ef76515de880d6da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Feb 2021 07:48:58 +1100 Subject: [PATCH 09/23] Only draw each rectangle outline pixel once --- .../imagedraw_rectangle_translucent_outline.png | Bin 0 -> 235 bytes Tests/test_imagedraw.py | 14 ++++++++++++++ src/libImaging/Draw.c | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 Tests/images/imagedraw_rectangle_translucent_outline.png diff --git a/Tests/images/imagedraw_rectangle_translucent_outline.png b/Tests/images/imagedraw_rectangle_translucent_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..845648762ccab1e1d2047f653a9bb6bda4e22e65 GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^DImhline(im, x0, y0 + i, x1, ink); draw->hline(im, x0, y1 - i, x1, ink); - draw->line(im, x1 - i, y0, x1 - i, y1, ink); - draw->line(im, x0 + i, y1, x0 + i, y0, ink); + draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink); + draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink); } } From 61ee8ec03cc9f12810e239d1618047cd7330d800 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 30 Dec 2020 03:27:28 +0100 Subject: [PATCH 10/23] document and add tests for SBIX color font support --- .github/workflows/test-windows.yml | 2 +- Tests/fonts/LICENSE.txt | 2 ++ Tests/fonts/chromacheck-sbix.woff | Bin 0 -> 740 bytes Tests/images/chromacheck-sbix.png | Bin 0 -> 1410 bytes Tests/images/chromacheck-sbix_mask.png | Bin 0 -> 1415 bytes Tests/test_imagefont.py | 40 +++++++++++++++++++++++++ docs/reference/ImageDraw.rst | 10 +++---- docs/releasenotes/8.0.0.rst | 3 +- 8 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 Tests/fonts/chromacheck-sbix.woff create mode 100644 Tests/images/chromacheck-sbix.png create mode 100644 Tests/images/chromacheck-sbix_mask.png diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index f3bb85f32..330db7c65 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -110,7 +110,7 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libwebp.cmd" - # for FreeType CBDT font support + # for FreeType CBDT/SBIX font support - name: Build dependencies / libpng if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 06eaa9a4e..884f6a5bf 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -15,6 +15,8 @@ FreeMono.ttf is licensed under GPLv3, with the GPL font exception. OpenSansCondensed-LightItalic.tt, from https://fonts.google.com/specimen/Open+Sans, under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +chromacheck-sbix.woff, from https://github.com/RoelN/ChromaCheck, under The MIT License (MIT), Copyright (c) 2018 Roel Nieskens, https://pixelambacht.nl Copyright (c) 2018 Google LLC + DejaVuSans-24-{1,2,4,8}-stripped.ttf are based on DejaVuSans.ttf converted using FontForge to add bitmap strikes and keep only the ASCII range. diff --git a/Tests/fonts/chromacheck-sbix.woff b/Tests/fonts/chromacheck-sbix.woff new file mode 100644 index 0000000000000000000000000000000000000000..518d4b7ea6c45b7c3c660ef94aa6b71affa9fa70 GIT binary patch literal 740 zcmXT-cXMN4WB>xDCk)&mnmGYTU4j5$C_DxI*pFoa zFMC0LaS70GHb6cPNF4(+14D6ACeRKh49lLy?gieV(neV{1Cf6`;7E?e*CY!ubQsB?9d{$1L`WtPRA`}Ggf|lf4pT1 zJ3|}i2C!Sw4gf8wWnclhi^Yk};9268#19OYm;(MZGjec!x+H4F{O3;P&Luk`i&?MD$f~p69W~x5zbY02Qr}@)!CD**2oiA59uI$vD+NvI$ z+t77`sm3X0xnYAtT+4Zv2_NU_^(l&OzbBKzvU7pw!~ef*_}LXEi#>3b6Xth5l*#7S Xof)d6z~~=tKfTnC)0e?ILG362-q6Sn literal 0 HcmV?d00001 diff --git a/Tests/images/chromacheck-sbix.png b/Tests/images/chromacheck-sbix.png new file mode 100644 index 0000000000000000000000000000000000000000..b906ef133a473b8f5bb3f777b0342acdf7a841cf GIT binary patch literal 1410 zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL985qF{<{ljGcd4vdb&7?h^t!3eJOz^LVrEgM07i zyyg6GeV_4d;k=3nhqeSp$tKnm4zUS}qnyz&7)=L5H80GWdGj8_hmQ8a?f#fwPCU}W T?Aix_Wh;ZHtDnm{r-UW|CpLkcdw+BWkigGfy{KSPE0h6bL4Ohzo+Cj@j9oCg;tIo|fc z%}SZo>IdR~$6kNM$!|00oH)orY}7fUDP$04WNgWD+sw$lj1NTl2V=p5aPJPr_uF`+ VU$07d4J=?8JYD@<);T3K0RVGNmXH7d literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 5d611a27f..757395bcf 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -897,6 +897,46 @@ class TestImageFont: assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or unsupported") + @skip_unless_feature_version("freetype2", "2.5.1") + def test_sbix(self): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", + size=300, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", embedded_color=True, font=font) + + with Image.open("Tests/images/chromacheck-sbix.png") as expected: + assert_image_similar(im, expected, 1) + except IOError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or unsupported") + + @skip_unless_feature_version("freetype2", "2.5.1") + def test_sbix_mask(self): + try: + font = ImageFont.truetype( + "Tests/fonts/chromacheck-sbix.woff", + size=300, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (400, 400), "white") + d = ImageDraw.Draw(im) + + d.text((50, 50), "\uE901", (100, 0, 0), font=font) + + with Image.open("Tests/images/chromacheck-sbix_mask.png") as expected: + assert_image_similar(im, expected, 1) + except IOError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("freetype compiled without libpng or unsupported") + @skip_unless_feature_version("freetype2", "2.10.0") def test_colr(self): font = ImageFont.truetype( diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 57d1c2dda..e2ef548d4 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -352,7 +352,7 @@ Methods .. versionadded:: 6.2.0 - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. versionadded:: 8.0.0 @@ -413,7 +413,7 @@ Methods .. versionadded:: 6.2.0 - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. versionadded:: 8.0.0 @@ -577,7 +577,7 @@ Methods correct substitutions as appropriate, if available. It should be a `BCP 47 language code`_. Requires libraqm. - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -626,7 +626,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param stroke_width: The width of the text stroke. - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) @@ -669,7 +669,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param stroke_width: The width of the text stroke. - :param embedded_color: Whether to use font embedded color glyphs (COLR or CBDT). + :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). .. py:method:: getdraw(im=None, hints=None) diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 1bef62e00..28dc8324d 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -115,8 +115,9 @@ now support fonts with embedded color data. To render text with embedded color data, use the parameter ``embedded_color=True``. Support for CBDT fonts requires FreeType 2.5 compiled with libpng. +Support for SBIX fonts requires FreeType 2.5.1 compiled with libpng. Support for COLR fonts requires FreeType 2.10. -SBIX and SVG fonts are not yet supported. +SVG fonts are not yet supported. ImageDraw.textlength ^^^^^^^^^^^^^^^^^^^^ From c709aa3d28abbdc006288ac88de082ba00439be0 Mon Sep 17 00:00:00 2001 From: nulano Date: Wed, 30 Dec 2020 04:48:01 +0100 Subject: [PATCH 11/23] minor test formatting cleanup --- Tests/test_imagefont.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 757395bcf..259a0f872 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -869,12 +869,12 @@ class TestImageFont: im = Image.new("RGB", (150, 150), "white") d = ImageDraw.Draw(im) - d.text((10, 10), "\U0001f469", embedded_color=True, font=font) + d.text((10, 10), "\U0001f469", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) - except IOError as e: + except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or unsupported") + pytest.skip("freetype compiled without libpng or CBDT support") @skip_unless_feature_version("freetype2", "2.5.0") def test_cbdt_mask(self): @@ -893,9 +893,9 @@ class TestImageFont: assert_image_similar_tofile( im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 ) - except IOError as e: + except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or unsupported") + pytest.skip("freetype compiled without libpng or CBDT support") @skip_unless_feature_version("freetype2", "2.5.1") def test_sbix(self): @@ -909,13 +909,13 @@ class TestImageFont: im = Image.new("RGB", (400, 400), "white") d = ImageDraw.Draw(im) - d.text((50, 50), "\uE901", embedded_color=True, font=font) + d.text((50, 50), "\uE901", font=font, embedded_color=True) with Image.open("Tests/images/chromacheck-sbix.png") as expected: assert_image_similar(im, expected, 1) - except IOError as e: + except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or unsupported") + pytest.skip("freetype compiled without libpng or SBIX support") @skip_unless_feature_version("freetype2", "2.5.1") def test_sbix_mask(self): @@ -933,9 +933,9 @@ class TestImageFont: with Image.open("Tests/images/chromacheck-sbix_mask.png") as expected: assert_image_similar(im, expected, 1) - except IOError as e: + except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") - pytest.skip("freetype compiled without libpng or unsupported") + pytest.skip("freetype compiled without libpng or SBIX support") @skip_unless_feature_version("freetype2", "2.10.0") def test_colr(self): @@ -948,7 +948,7 @@ class TestImageFont: im = Image.new("RGB", (300, 75), "white") d = ImageDraw.Draw(im) - d.text((15, 5), "Bungee", embedded_color=True, font=font) + d.text((15, 5), "Bungee", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21) From 8fb5fd7f633a64948c472e0716b0909a99e8029e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Feb 2021 12:14:49 +1100 Subject: [PATCH 12/23] Updated tests for changed helper imports --- Tests/test_imagefont.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 259a0f872..80ae55d32 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -911,8 +911,7 @@ class TestImageFont: d.text((50, 50), "\uE901", font=font, embedded_color=True) - with Image.open("Tests/images/chromacheck-sbix.png") as expected: - assert_image_similar(im, expected, 1) + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or SBIX support") @@ -931,8 +930,7 @@ class TestImageFont: d.text((50, 50), "\uE901", (100, 0, 0), font=font) - with Image.open("Tests/images/chromacheck-sbix_mask.png") as expected: - assert_image_similar(im, expected, 1) + assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) except IOError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or SBIX support") From 346bfc95375fe9441f904af21af1e6ddb3d2898d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Mar 2021 08:55:24 +1100 Subject: [PATCH 13/23] Added IPythonViewer --- Tests/test_imageshow.py | 18 +++++++++++++++++- src/PIL/ImageShow.py | 15 +++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 78e80f521..5981e22c0 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -62,4 +62,20 @@ def test_viewer(): def test_viewers(): for viewer in ImageShow._viewers: - viewer.get_command("test.jpg") + try: + viewer.get_command("test.jpg") + except NotImplementedError: + pass + + +def test_ipythonviewer(): + pytest.importorskip("IPython", reason="IPython not installed") + for viewer in ImageShow._viewers: + if isinstance(viewer, ImageShow.IPythonViewer): + test_viewer = viewer + break + else: + assert False + + im = hopper() + assert test_viewer.show(im) == 1 diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index fceb65378..9e7614144 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -225,6 +225,21 @@ if sys.platform not in ("win32", "darwin"): # unixoids if shutil.which("xv"): register(XVViewer) + +class IPythonViewer(Viewer): + def show_image(self, image, **options): + display(image) + return 1 + + +try: + from IPython.display import display +except ImportError: + pass +else: + register(IPythonViewer) + + if __name__ == "__main__": if len(sys.argv) < 2: From f067fe4c05bfed6e20257c3c54f77ade8547de86 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Mar 2021 08:56:03 +1100 Subject: [PATCH 14/23] Added import alias for clarity --- src/PIL/ImageShow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 9e7614144..bb04c4e29 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -228,12 +228,12 @@ if sys.platform not in ("win32", "darwin"): # unixoids class IPythonViewer(Viewer): def show_image(self, image, **options): - display(image) + ipython_display(image) return 1 try: - from IPython.display import display + from IPython.display import display as ipython_display except ImportError: pass else: From 5e0a4acb85bd3ea1dd2d41e25bc4616fde15b138 Mon Sep 17 00:00:00 2001 From: Kipkurui Mutai Date: Sun, 28 Feb 2021 19:50:27 +0300 Subject: [PATCH 15/23] Update ImageShow.rst --- docs/reference/ImageShow.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index a30a6caed..f1fbd90ce 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -9,6 +9,7 @@ All default viewers convert the image to be shown to PNG format. .. autofunction:: PIL.ImageShow.show +.. autoclass:: IPythonViewer .. autoclass:: WindowsViewer .. autoclass:: MacViewer From 7b094638094986a7506efb310b6dcbe72675276b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Mar 2021 08:56:49 +1100 Subject: [PATCH 16/23] Added IPythonViewer docstring --- src/PIL/ImageShow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index bb04c4e29..3368865a4 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -227,6 +227,8 @@ if sys.platform not in ("win32", "darwin"): # unixoids class IPythonViewer(Viewer): + """The viewer for IPython frontends.""" + def show_image(self, image, **options): ipython_display(image) return 1 From a1463ff211c37a026135894487d13f7c908ed8ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 Mar 2021 08:59:47 +1100 Subject: [PATCH 17/23] Added release notes --- docs/releasenotes/8.2.0.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 28d39ca46..f27f295a7 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -21,10 +21,17 @@ TODO API Additions ============= -TODO -^^^^ +ImageShow.IPythonViewer +^^^^^^^^^^^^^^^^^^^^^^^ -TODO +If IPython is present, this new ``ImageShow.Viewer`` subclass will be +registered. It displays images on all IPython frontends. This will be helpful +to users of Google Colab, allowing ``im.show()`` to display images. + +It is lower in priority than the other default Viewer instances, so it will +only be used by ``im.show()`` or ``ImageShow.show()`` if none of the other +viewers are available. This means that the behaviour of ``ImageShow`` will stay +the same for most Pillow users. Security ======== From 441a1cf9cf7a6fa018e7cecdfbd70b5680b53f26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Mar 2021 19:00:57 +1100 Subject: [PATCH 18/23] Update CHANGES.rst [ci skip] --- CHANGES.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 50401e2c3..aa25e7991 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,21 @@ Changelog (Pillow) 8.2.0 (unreleased) ------------------ +- Added IPythonViewer #5289 + [radarhere, Kipkurui-mutai] + +- Only draw each rectangle outline pixel once #5183 + [radarhere] + +- Use mmap instead of built-in Win32 mapper #5224 + [radarhere, cgohlke] + +- Handle PCX images with an odd stride #5214 + [radarhere] + +- Only read different sizes for "Large Thumbnail" MPO frames #5168 + [radarhere] + - Added PyQt6 support #5258 [radarhere] From f5d49f4f6166625fec381dc28886258a8be19f06 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Mar 2021 19:53:59 +1100 Subject: [PATCH 19/23] Added rounded_rectangle method --- Tests/images/imagedraw_rounded_rectangle.png | Bin 0 -> 934 bytes Tests/test_imagedraw.py | 28 ++++++++++++ docs/reference/ImageDraw.rst | 14 ++++++ src/PIL/ImageDraw.py | 45 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 Tests/images/imagedraw_rounded_rectangle.png diff --git a/Tests/images/imagedraw_rounded_rectangle.png b/Tests/images/imagedraw_rounded_rectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..2e815f4ada247e8302c8cc5e053a162b4fe3ed01 GIT binary patch literal 934 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV9xb)aSW-L^Y+HYqE!YWZGkra z`|j^~zUcByEkULy+m*8KcJ6r0b6;iKWfq_w7JO{=uCm%MWtMrNFZA{Hn$kOmx$mrcylh_ev~WwS zgty)=UgoY9+4kq%s}Iie>-O!L=l3Kt@%*~)XYTyJsLt*EYRRoiQ~ffF50Ce5U9#%j z>c4MhiCe$nUA=YQs&jb>(YyN&-SnFD%b+*8urBrDH`(lM4Xf<@AJ06uUgws?wVYk6 z?5-Z%*7onYY+VlLYB~KI>+Eh%^j&Se=B7(%?7?^2%H&#C?dq#Ne=A={>DQrEyTiAw zUR_!9=<2SQ^JKd_`Zj3&Q?-~MRayVJ@2Sk5Woysh4fJT)&=JAC`dyqQ`}&;A_uI;J zlv-SYVgh1pg*rZu#DecshQo!%8SzkmVTE(;2wjg~sgADZUY zzLQPNopVC<%BkOdPLLSzJG}blm+ueEt;Gbb4{yqhJ-#pft}HMnx2;?7amw@XsBLc| zHh13om9u;GtUt$;Qj#OSRvcXRb!A^p_~m)B*=DiP^PeY#UN*T~w(d^Uv+T#W_ALBn zc5%;+jQ9C#u6b=+w^=&$YRQY^)!DYWbFUv*wd}9^!&`^?jtW=qlC{}=&$#%%`|oMt zH*QB+#!b5)D_i&Ca`e8IL-$;+pPvdzun)FNS@!G?Og_f*_k4A)%rW^DiIdyjMeGmF zGI)IT$Ha{r_Jx+&xQAvHSgg4Z&K>`Lal^6 Date: Mon, 22 Feb 2021 07:38:04 +1100 Subject: [PATCH 20/23] Only draw each pixel once --- .../imagedraw_rounded_rectangle_both.png | Bin 0 -> 530 bytes .../images/imagedraw_rounded_rectangle_x.png | Bin 0 -> 567 bytes .../images/imagedraw_rounded_rectangle_y.png | Bin 0 -> 528 bytes Tests/test_imagedraw.py | 24 ++++ src/PIL/ImageDraw.py | 103 +++++++++++++----- 5 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_both.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_x.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_y.png diff --git a/Tests/images/imagedraw_rounded_rectangle_both.png b/Tests/images/imagedraw_rounded_rectangle_both.png new file mode 100644 index 0000000000000000000000000000000000000000..24f600e3913ebc74a9c301e815f06f4ac0cfdd94 GIT binary patch literal 530 zcmeAS@N?(olHy`uVBq!ia0vp^DIm7ZFWD}C_M}wCs{)<;eRDP)o0Zj@@g?*-%YXh2Cub>!tu2gw`02!~<0084 z-RnK1`%I^)u!s%^Wrko*Ci z>}`Huu3gs7ZhQOk`^z=c%tQ=M%qsIWS=Y?1;;o>R-BzHQ_2I-V^_n&7FNirrZ+iA| z>TR8qb5tKpF?HUtsJZ~?m)F{Rqb-XLEvk_Jv-s5<*OYTsYmCGKebp|D?4uWRojEl-z|T`s&ch> literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_x.png b/Tests/images/imagedraw_rounded_rectangle_x.png new file mode 100644 index 0000000000000000000000000000000000000000..4bf5211a3340d62724b64a5517693c1fa2403e7d GIT binary patch literal 567 zcmeAS@N?(olHy`uVBq!ia0vp^DImsnv^Py)@I#H6G!aHYxGIp1VMO3=9uSE2Lu&$j^^gx8G4e{Yv2p zkI3asQ9VNDZr3i)l3N)brgi#JhWE^^-xt*;EnB3$euc-isrH`>r}fR}?p~rSHoY|G z)s>6WlCuNjBO`T#{Vi@?+BEI6=#;J5rxv}O7@MjXzmj?1v5@Su7beP23{88L@U%t0 zUOI2}narq-xt{scwG|aEzKp7;#;jQ_5L91BvpZlrp`NrOe<1yh3&3|zc8+e-t_F_ zROyHVGgTi<0ZJ_@HaK%q>b175p^DMd6%}G<*JfP{d~)X2S|jm|n^pz@L+woX(?2?D zn>h_W`!{4xVk&ixUF^)86}S4T?ZM~&{BLr5q-k>-Y>jTH`~Z%~^+)udCACbFI5_JE Oi0|p@=d#Wzp$Pyx_3|kI literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_y.png b/Tests/images/imagedraw_rounded_rectangle_y.png new file mode 100644 index 0000000000000000000000000000000000000000..9b391b95e28ea723f75e90210945f1481d93d402 GIT binary patch literal 528 zcmeAS@N?(olHy`uVBq!ia0vp^DIm37 zx6T&5yXNj1ekQKN^_ScyZq&GA?4T4MP&VO)CM!@M7`Rka-?Xl+W8L@TeEEa!|J}W! zQZKw>cC3i#Pr23Q@l59RM>qGrtM~54Wu6K;S{iCM`Re8c{U?e)bai zaw}?Hd1Vm3RHWIrXHFfVaZ&s7&=$$1nAL zofUm1{en% z&%y@vNY;7$Q~ax)mtGGqnm7N!^FOmL6o&p;-`r5oBlscS1QtCv}Ao?m#zm1 NdAj= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) + if d == 0: + # If the corners have no curve, that is a rectangle return self.rectangle(xy, fill, outline, width) ink, fill = self._getink(outline, fill) + + def draw_corners(pieslice): + if full_x: + # Draw top and bottom halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 180, 360), + ((x0, y1 - d, x0 + d, y1), 0, 180), + ) + elif full_y: + # Draw left and right halves + parts = ( + ((x0, y0, x0 + d, y0 + d), 90, 270), + ((x1 - d, y0, x1, y0 + d), 270, 90), + ) + else: + # Draw four separate corners + parts = ( + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ((x0, y0, x0 + d, y0 + d), 180, 270), + ) + for part in parts: + if pieslice: + self.draw.draw_pieslice(*(part + (fill, 1))) + else: + self.draw.draw_arc(*(part + (ink, width))) + if fill is not None: - self.draw.draw_pieslice((x1 - d, y0, x1, y0 + d), 270, 360, fill, 1) - self.draw.draw_pieslice((x1 - d, y1 - d, x1, y1), 0, 90, fill, 1) - self.draw.draw_pieslice((x0, y1 - d, x0 + d, y1), 90, 180, fill, 1) - self.draw.draw_pieslice((x0, y0, x0 + d, y0 + d), 180, 270, fill, 1) + draw_corners(True) - self.draw.draw_rectangle((x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1) - self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1 - ) - self.draw.draw_rectangle( - (x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 - ) + if full_x: + self.draw.draw_rectangle( + (x0, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 + ) + else: + self.draw.draw_rectangle( + (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1 + ) + if not full_x and not full_y: + self.draw.draw_rectangle( + (x0, y0 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1 + ) + self.draw.draw_rectangle( + (x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 + ) if ink is not None and ink != fill and width != 0: - self.draw.draw_arc((x1 - d, y0, x1, y0 + d), 270, 360, ink, width) - self.draw.draw_arc((x1 - d, y1 - d, x1, y1), 0, 90, ink, width) - self.draw.draw_arc((x0, y1 - d, x0 + d, y1), 90, 180, ink, width) - self.draw.draw_arc((x0, y0, x0 + d, y0 + d), 180, 270, ink, width) + draw_corners(False) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1 - ) - self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1 - ) + if not full_x: + self.draw.draw_rectangle( + (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1 + ) + self.draw.draw_rectangle( + (x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1 + ) + if not full_y: + self.draw.draw_rectangle( + (x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1 + ) + self.draw.draw_rectangle( + (x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1 + ) def _multiline_check(self, text): """Draw text.""" From 62bf920634164509f2465b2ae1d7924bc7065699 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Mar 2021 20:10:17 +1100 Subject: [PATCH 21/23] Added release notes [ci skip] --- docs/releasenotes/8.2.0.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index f27f295a7..04d80e80e 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -13,10 +13,20 @@ when Tk/Tcl 8.5 will be the minimum supported. API Changes =========== -TODO -^^^^ +ImageDraw.rounded_rectangle +^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as +:py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` +argument. ``radius`` is limited to half of the width or the height, so that users can +create a circle, but not any other ellipse. + +.. code-block:: python + + from PIL import Image, ImageDraw + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") API Additions ============= From e7f5bb18312e6c6db1ec3cd4eec7272be094baaf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 Feb 2021 18:19:05 +1100 Subject: [PATCH 22/23] Ensure file is closed if it is opened by ImageQt.ImageQt --- Tests/test_imageqt.py | 13 +++++++++++-- src/PIL/ImageQt.py | 19 +++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 404849cb9..53b1fef7c 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -4,11 +4,14 @@ from PIL import ImageQt from .helper import hopper +pytestmark = pytest.mark.skipif( + not ImageQt.qt_is_installed, reason="Qt bindings are not installed" +) + if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba -@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_rgb(): # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb @@ -38,7 +41,13 @@ def test_rgb(): checkrgb(0, 0, 255) -@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_image(): for mode in ("1", "RGB", "RGBA", "L", "P"): ImageQt.ImageQt(hopper(mode)) + + +def test_closed_file(): + with pytest.warns(None) as record: + ImageQt.ImageQt("Tests/images/hopper.gif") + + assert not record diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 74ca3166c..56650e102 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -128,6 +128,7 @@ def align8to32(bytes, width, mode): def _toqclass_helper(im): data = None colortable = None + exclusive_fp = False # handle filename, if given instead of image name if hasattr(im, "toUtf8"): @@ -135,6 +136,7 @@ def _toqclass_helper(im): im = str(im.toUtf8(), "utf-8") if isPath(im): im = Image.open(im) + exclusive_fp = True qt_format = QImage.Format if qt_version == "6" else QImage if im.mode == "1": @@ -157,10 +159,15 @@ def _toqclass_helper(im): data = im.tobytes("raw", "BGRA") format = qt_format.Format_ARGB32 else: + if exclusive_fp: + im.close() raise ValueError(f"unsupported image mode {repr(im.mode)}") - __data = data or align8to32(im.tobytes(), im.size[0], im.mode) - return {"data": __data, "im": im, "format": format, "colortable": colortable} + size = im.size + __data = data or align8to32(im.tobytes(), size[0], im.mode) + if exclusive_fp: + im.close() + return {"data": __data, "size": size, "format": format, "colortable": colortable} if qt_is_installed: @@ -182,8 +189,8 @@ if qt_is_installed: self.__data = im_data["data"] super().__init__( self.__data, - im_data["im"].size[0], - im_data["im"].size[1], + im_data["size"][0], + im_data["size"][1], im_data["format"], ) if im_data["colortable"]: @@ -197,8 +204,8 @@ def toqimage(im): def toqpixmap(im): # # This doesn't work. For now using a dumb approach. # im_data = _toqclass_helper(im) - # result = QPixmap(im_data['im'].size[0], im_data['im'].size[1]) - # result.loadFromData(im_data['data']) + # result = QPixmap(im_data["size"][0], im_data["size"][1]) + # result.loadFromData(im_data["data"]) # Fix some strange bug that causes if im.mode == "RGB": im = im.convert("RGBA") From 666d3c5d3f2c042fef3a2f2976458a67bf72e245 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 Mar 2021 22:21:22 +1100 Subject: [PATCH 23/23] Use bash shell for mkdir command --- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 12c288374..32518c9d1 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -174,7 +174,7 @@ jobs: if: failure() run: | mkdir -p Tests/errors - shell: pwsh + shell: bash - name: Upload errors uses: actions/upload-artifact@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4064a0589..e52fefc69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,6 @@ jobs: if: failure() run: | mkdir -p Tests/errors - shell: pwsh - name: Upload errors uses: actions/upload-artifact@v2