From 023dcf27c29e2b62c84b171d1473bdebd5fcf0eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jul 2019 08:37:17 +1000 Subject: [PATCH 01/54] Raise the same error if a truncated image is loaded a second time --- Tests/test_file_jpeg.py | 4 ++++ Tests/test_imagefile.py | 4 ++++ src/PIL/ImageFile.py | 1 - src/PIL/PngImagePlugin.py | 3 ++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4ade11a2966..a9ba54dd1c1 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -372,6 +372,10 @@ def test_truncated_jpeg_throws_IOError(self): with self.assertRaises(IOError): im.load() + # Test that the error is raised if loaded a second time + with self.assertRaises(IOError): + im.load() + def _n_qtables_helper(self, n, test_file): im = Image.open(test_file) f = self.tempfile("temp.jpg") diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 83170cb2ab5..87bba7c2f1e 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -113,6 +113,10 @@ def test_truncated_with_errors(self): with self.assertRaises(IOError): im.load() + # Test that the error is raised if loaded a second time + with self.assertRaises(IOError): + im.load() + def test_truncated_without_errors(self): if "zip_encoder" not in codecs: self.skipTest("PNG (zlib) encoder not available") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index e5173a1fb75..49d8ef97ceb 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -243,7 +243,6 @@ def load(self): if LOAD_TRUNCATED_IMAGES: break else: - self.tile = [] raise IOError( "image file is truncated " "(%d bytes not processed)" % len(b) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 10e18e4a071..f69ca046633 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -612,7 +612,7 @@ def _open(self): rawmode, data = self.png.im_palette self.palette = ImagePalette.raw(rawmode, data) - self.__idat = length # used by load_read() + self.__prepare_idat = length # used by load_prepare() @property def text(self): @@ -645,6 +645,7 @@ def load_prepare(self): if self.info.get("interlace"): self.decoderconfig = self.decoderconfig + (1,) + self.__idat = self.__prepare_idat # used by load_read() ImageFile.ImageFile.load_prepare(self) def load_read(self, read_bytes): From 2995fb67c1770af679216380e1ea12c0c33aeee2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 Jul 2019 13:50:13 +1000 Subject: [PATCH 02/54] Return after error --- Tests/test_file_jpeg2k.py | 6 ++++++ src/encode.c | 2 ++ 2 files changed, 8 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a2483fade2f..3ca99486c5f 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -90,6 +90,12 @@ def test_tiled_offset_rt(self): ) self.assert_image_equal(im, test_card) + def test_tiled_offset_too_small(self): + with self.assertRaises(ValueError): + self.roundtrip( + test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32) + ) + def test_irreversible_rt(self): im = self.roundtrip(test_card, irreversible=True, quality_layers=[20]) self.assert_image_similar(im, test_card, 2.0) diff --git a/src/encode.c b/src/encode.c index ac729f455b1..7dc1035c499 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1211,6 +1211,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) PyErr_SetString(PyExc_ValueError, "JPEG 2000 tile offset too small; top left tile must " "intersect image area"); + Py_DECREF(encoder); + return NULL; } if (context->tile_offset_x > context->offset_x From 0427170db5e0ecd55e2b42013c3c9e13e011d2f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 Jul 2019 19:04:35 +1000 Subject: [PATCH 03/54] Documented more encoding values --- src/PIL/ImageFont.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index f43f95b9ac9..a33bacade07 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -528,11 +528,23 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): the loader also looks in Windows :file:`fonts/` directory. :param size: The requested size, in points. :param index: Which font face to load (default is first available face). - :param encoding: Which font encoding to use (default is Unicode). Common - encodings are "unic" (Unicode), "symb" (Microsoft - Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), - and "armn" (Apple Roman). See the FreeType documentation - for more information. + :param encoding: Which font encoding to use (default is Unicode). Possible + encodings include: + + * "unic" (Unicode) + * "symb" (Microsoft Symbol) + * "ADOB" (Adobe Standard) + * "ADBE" (Adobe Expert) + * "ADBC" (Adobe Custom) + * "armn" (Apple Roman) + * "sjis" (Shift JIS) + * "gb " (PRC) + * "big5" + * "wans" (Extended Wansung) + * "joha" (Johab) + * "lat1" (Latin-1) + + See the FreeType documentation for more information. :param layout_engine: Which layout engine to use, if available: `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`. :return: A font object. From c76369ce874c7a4b0928495bb7e17f9759aa4a40 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 Jul 2019 19:28:54 +1000 Subject: [PATCH 04/54] Explain that encoding does not alter text --- src/PIL/ImageFont.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a33bacade07..1f8c7efad63 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -529,7 +529,8 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :param size: The requested size, in points. :param index: Which font face to load (default is first available face). :param encoding: Which font encoding to use (default is Unicode). Possible - encodings include: + encodings include (see the FreeType documentation for more + information): * "unic" (Unicode) * "symb" (Microsoft Symbol) @@ -544,7 +545,8 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): * "joha" (Johab) * "lat1" (Latin-1) - See the FreeType documentation for more information. + This specifies the character set to use. It does not alter the + encoding of any text provided in subsequent operations. :param layout_engine: Which layout engine to use, if available: `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`. :return: A font object. From f93a5d09728adfa0ea7e3ae52f9234c097d5e824 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 Jul 2019 06:40:03 +1000 Subject: [PATCH 05/54] Added text stroking --- Tests/images/imagedraw_stroke_different.png | Bin 0 -> 2259 bytes Tests/images/imagedraw_stroke_multiline.png | Bin 0 -> 4061 bytes Tests/images/imagedraw_stroke_same.png | Bin 0 -> 1328 bytes Tests/images/test_direction_ttb_stroke.png | Bin 0 -> 3749 bytes Tests/test_imagedraw.py | 54 +++++++- Tests/test_imagefont.py | 15 +++ Tests/test_imagefontctl.py | 24 ++++ docs/reference/ImageDraw.rst | 23 +++- src/PIL/ImageDraw.py | 134 +++++++++++++++++--- src/PIL/ImageFont.py | 62 +++++++-- src/_imagingft.c | 96 ++++++++++---- 11 files changed, 354 insertions(+), 54 deletions(-) create mode 100644 Tests/images/imagedraw_stroke_different.png create mode 100644 Tests/images/imagedraw_stroke_multiline.png create mode 100644 Tests/images/imagedraw_stroke_same.png create mode 100644 Tests/images/test_direction_ttb_stroke.png diff --git a/Tests/images/imagedraw_stroke_different.png b/Tests/images/imagedraw_stroke_different.png new file mode 100644 index 0000000000000000000000000000000000000000..e58cbdc4e23ac1df1d865916a0ae10535fe18f04 GIT binary patch literal 2259 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 X<7$W=>Ca!;PGzWjPp3-DDeC_KbrPo| literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_stroke_same.png b/Tests/images/imagedraw_stroke_same.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2f3abe1a6f263e45b0ec721bfad51792348e59 GIT binary patch literal 1328 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 literal 0 HcmV?d00001 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ffe35a4fa4f..ed4291f53ff 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,8 +1,8 @@ import os.path -from PIL import Image, ImageColor, ImageDraw +from PIL import Image, ImageColor, ImageDraw, ImageFont, features -from .helper import PillowTestCase, hopper +from .helper import PillowTestCase, hopper, unittest BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -29,6 +29,8 @@ KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +HAS_FREETYPE = features.check("freetype2") + class TestImageDraw(PillowTestCase): def test_sanity(self): @@ -771,6 +773,54 @@ def test_textsize_empty_string(self): draw.textsize("\n") draw.textsize("test\n") + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_textsize_stroke(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) + + # Act / Assert + self.assertEqual(draw.textsize("A", font, stroke_width=2), (16, 20)) + self.assertEqual( + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2), (52, 44) + ) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_stroke(self): + for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): + # Arrange + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.text( + (10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill + ) + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 2.8 + ) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_stroke_multiline(self): + # Arrange + im = Image.new("RGB", (100, 250)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + + # Act + draw.multiline_text( + (10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0" + ) + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3 + ) + def test_same_color_outline(self): # Prepare shape x0, y0 = 5, 5 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 8a23e6339a3..6a2d572a954 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -605,6 +605,21 @@ def test_imagefont_getters(self): self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 36)) self.assertEqual(t.getsize_multiline("ABC\nAaaa"), (48, 36)) + def test_getsize_stroke(self): + # Arrange + t = self.get_font() + + # Act / Assert + for stroke_width in [0, 2]: + self.assertEqual( + t.getsize("A", stroke_width=stroke_width), + (12 + stroke_width * 2, 16 + stroke_width * 2), + ) + self.assertEqual( + t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width), + (48 + stroke_width * 2, 36 + stroke_width * 4), + ) + def test_complex_font_settings(self): # Arrange t = self.get_font() diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index afd45ce1982..5b88f94cced 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -115,6 +115,30 @@ def test_text_direction_ttb(self): self.assert_image_similar(im, target_img, 1.15) + def test_text_direction_ttb_stroke(self): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) + + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text( + (25, 25), + "あい", + font=ttf, + fill=500, + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", + ) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + self.skipTest("libraqm 0.7 or greater not available") + + target = "Tests/images/test_direction_ttb_stroke.png" + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, 12.4) + def test_ligature_features(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 5fac7914b6f..51eaf925ec6 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -255,7 +255,7 @@ Methods Draw a shape. -.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None) +.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None) Draws the string at the given position. @@ -297,6 +297,15 @@ Methods .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + + :param stroke_fill: Color to use for the text stroke. If not given, will default to + the ``fill`` parameter. + + .. versionadded:: 6.2.0 + .. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None) Draws the string at the given position. @@ -336,7 +345,7 @@ Methods .. versionadded:: 6.0.0 -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None) +.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. @@ -372,7 +381,11 @@ Methods .. versionadded:: 6.0.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None) + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) Return the size of the given string, in pixels. @@ -408,6 +421,10 @@ Methods .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + .. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) .. warning:: This method is experimental. diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c9b2773881a..f51578c10f1 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -261,24 +261,95 @@ def _multiline_split(self, text): return text.split(split_character) - def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs): + def text( + self, + xy, + text, + fill=None, + font=None, + anchor=None, + spacing=4, + align="left", + direction=None, + features=None, + language=None, + stroke_width=0, + stroke_fill=None, + *args, + **kwargs + ): if self._multiline_check(text): - return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs) - ink, fill = self._getink(fill) + return self.multiline_text( + xy, + text, + fill, + font, + anchor, + spacing, + align, + direction, + features, + language, + stroke_width, + stroke_fill, + ) + if font is None: font = self.getfont() - if ink is None: - ink = fill - if ink is not None: + + def getink(fill): + ink, fill = self._getink(fill) + if ink is None: + return fill + return ink + + def drawText(ink, stroke_width=0, stroke_offset=None): + coord = xy try: - mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs) - xy = xy[0] + offset[0], xy[1] + offset[1] + mask, offset = font.getmask2( + text, + self.fontmode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, + *args, + **kwargs + ) + coord = coord[0] + offset[0], coord[1] + offset[1] except AttributeError: try: - mask = font.getmask(text, self.fontmode, *args, **kwargs) + mask = font.getmask( + text, + self.fontmode, + direction, + features, + language, + stroke_width, + *args, + **kwargs + ) except TypeError: mask = font.getmask(text) - self.draw.draw_bitmap(xy, mask, ink) + if stroke_offset: + coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + self.draw.draw_bitmap(coord, mask, ink) + + ink = getink(fill) + if ink is not None: + stroke_ink = None + if stroke_width: + stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink + + if stroke_ink is not None: + # Draw stroked text + drawText(stroke_ink, stroke_width) + + # Draw normal text + drawText(ink, 0, (stroke_width, stroke_width)) + else: + # Only draw normal text + drawText(ink) def multiline_text( self, @@ -292,14 +363,23 @@ def multiline_text( direction=None, features=None, language=None, + stroke_width=0, + stroke_fill=None, ): widths = [] max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize("A", font=font)[1] + spacing + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) for line in lines: line_width, line_height = self.textsize( - line, font, direction=direction, features=features, language=language + line, + font, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, ) widths.append(line_width) max_width = max(max_width, line_width) @@ -322,32 +402,50 @@ def multiline_text( direction=direction, features=features, language=language, + stroke_width=stroke_width, + stroke_fill=stroke_fill, ) top += line_spacing left = xy[0] def textsize( - self, text, font=None, spacing=4, direction=None, features=None, language=None + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, ): """Get the size of a given string, in pixels.""" if self._multiline_check(text): return self.multiline_textsize( - text, font, spacing, direction, features, language + text, font, spacing, direction, features, language, stroke_width ) if font is None: font = self.getfont() - return font.getsize(text, direction, features, language) + return font.getsize(text, direction, features, language, stroke_width) def multiline_textsize( - self, text, font=None, spacing=4, direction=None, features=None, language=None + self, + text, + font=None, + spacing=4, + direction=None, + features=None, + language=None, + stroke_width=0, ): max_width = 0 lines = self._multiline_split(text) - line_spacing = self.textsize("A", font=font)[1] + spacing + line_spacing = ( + self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing + ) for line in lines: line_width, line_height = self.textsize( - line, font, spacing, direction, features, language + line, font, spacing, direction, features, language, stroke_width ) max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index e2e6af33254..737ced4724a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -207,7 +207,9 @@ def getmetrics(self): """ return self.font.ascent, self.font.descent - def getsize(self, text, direction=None, features=None, language=None): + def getsize( + self, text, direction=None, features=None, language=None, stroke_width=0 + ): """ Returns width and height (in pixels) of given text if rendered in font with provided direction, features, and language. @@ -243,13 +245,26 @@ def getsize(self, text, direction=None, features=None, language=None): .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: (width, height) """ size, offset = self.font.getsize(text, direction, features, language) - return (size[0] + offset[0], size[1] + offset[1]) + return ( + size[0] + stroke_width * 2 + offset[0], + size[1] + stroke_width * 2 + offset[1], + ) def getsize_multiline( - self, text, direction=None, spacing=4, features=None, language=None + self, + text, + direction=None, + spacing=4, + features=None, + language=None, + stroke_width=0, ): """ Returns width and height (in pixels) of given text if rendered in font @@ -285,13 +300,19 @@ def getsize_multiline( .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: (width, height) """ max_width = 0 lines = self._multiline_split(text) - line_spacing = self.getsize("A")[1] + spacing + line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing for line in lines: - line_width, line_height = self.getsize(line, direction, features, language) + line_width, line_height = self.getsize( + line, direction, features, language, stroke_width + ) max_width = max(max_width, line_width) return max_width, len(lines) * line_spacing - spacing @@ -308,7 +329,15 @@ def getoffset(self, text): """ return self.font.getsize(text)[1] - def getmask(self, text, mode="", direction=None, features=None, language=None): + def getmask( + self, + text, + mode="", + direction=None, + features=None, + language=None, + stroke_width=0, + ): """ Create a bitmap for the text. @@ -352,11 +381,20 @@ def getmask(self, text, mode="", direction=None, features=None, language=None): .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ return self.getmask2( - text, mode, direction=direction, features=features, language=language + text, + mode, + direction=direction, + features=features, + language=language, + stroke_width=stroke_width, )[0] def getmask2( @@ -367,6 +405,7 @@ def getmask2( direction=None, features=None, language=None, + stroke_width=0, *args, **kwargs ): @@ -413,13 +452,20 @@ def getmask2( .. versionadded:: 6.0.0 + :param stroke_width: The width of the text stroke. + + .. versionadded:: 6.2.0 + :return: A tuple of an internal PIL storage memory instance as defined by the :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 = size[0] + stroke_width * 2, size[1] + stroke_width * 2 im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1", direction, features, language) + self.font.render( + text, im.id, mode == "1", direction, features, language, stroke_width + ) return im, offset def font_variant( diff --git a/src/_imagingft.c b/src/_imagingft.c index 87376383e3b..7776e43f1b7 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -25,6 +25,7 @@ #include #include FT_FREETYPE_H #include FT_GLYPH_H +#include FT_STROKER_H #include FT_MULTIPLE_MASTERS_H #include FT_SFNT_NAMES_H @@ -790,7 +791,13 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender, horizontal_dir; int load_flags; unsigned char *source; - FT_GlyphSlot glyph; + FT_Glyph glyph; + FT_GlyphSlot glyph_slot; + FT_Bitmap bitmap; + FT_BitmapGlyph bitmap_glyph; + int stroke_width = 0; + FT_Stroker stroker = NULL; + FT_Int left; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ PyObject* string; @@ -806,7 +813,8 @@ font_render(FontObject* self, PyObject* args) GlyphInfo *glyph_info; PyObject *features = NULL; - if (!PyArg_ParseTuple(args, "On|izOz:render", &string, &id, &mask, &dir, &features, &lang)) { + if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang, + &stroke_width)) { return NULL; } @@ -819,21 +827,37 @@ font_render(FontObject* self, PyObject* args) Py_RETURN_NONE; } + if (stroke_width) { + error = FT_Stroker_New(library, &stroker); + if (error) { + return geterror(error); + } + + FT_Stroker_Set(stroker, (FT_Fixed)stroke_width*64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); + } + im = (Imaging) id; /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; - if (mask) + load_flags = FT_LOAD_NO_BITMAP; + if (stroker == NULL) { + load_flags |= FT_LOAD_RENDER; + } + if (mask) { load_flags |= FT_LOAD_TARGET_MONO; + } ascender = 0; for (i = 0; i < count; i++) { index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + if (error) { return geterror(error); + } - glyph = self->face->glyph; - temp = glyph->bitmap.rows - glyph->bitmap_top; + glyph_slot = self->face->glyph; + bitmap = glyph_slot->bitmap; + + temp = bitmap.rows - glyph_slot->bitmap_top; temp -= PIXEL(glyph_info[i].y_offset); if (temp > ascender) ascender = temp; @@ -844,37 +868,62 @@ font_render(FontObject* self, PyObject* args) for (i = 0; i < count; i++) { index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); - if (error) + if (error) { return geterror(error); + } + + glyph_slot = self->face->glyph; + if (stroker != NULL) { + error = FT_Get_Glyph(glyph_slot, &glyph); + if (!error) { + error = FT_Glyph_Stroke(&glyph, stroker, 1); + } + if (!error) { + FT_Vector origin = {0, 0}; + error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); + } + if (error) { + return geterror(error); + } + + bitmap_glyph = (FT_BitmapGlyph)glyph; + + bitmap = bitmap_glyph->bitmap; + left = bitmap_glyph->left; + + FT_Done_Glyph(glyph); + } else { + bitmap = glyph_slot->bitmap; + left = glyph_slot->bitmap_left; + } - glyph = self->face->glyph; if (horizontal_dir) { - if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) { - x = -self->face->glyph->metrics.horiBearingX; + if (i == 0 && glyph_slot->metrics.horiBearingX < 0) { + x = -glyph_slot->metrics.horiBearingX; } - xx = PIXEL(x) + glyph->bitmap_left; - xx += PIXEL(glyph_info[i].x_offset); + xx = PIXEL(x) + left; + xx += PIXEL(glyph_info[i].x_offset) + stroke_width; } else { - if (self->face->glyph->metrics.vertBearingX < 0) { - x = -self->face->glyph->metrics.vertBearingX; + if (glyph_slot->metrics.vertBearingX < 0) { + x = -glyph_slot->metrics.vertBearingX; } - xx = im->xsize / 2 - glyph->bitmap.width / 2; + xx = im->xsize / 2 - bitmap.width / 2; } x0 = 0; - x1 = glyph->bitmap.width; + x1 = bitmap.width; if (xx < 0) x0 = -xx; if (xx + x1 > im->xsize) x1 = im->xsize - xx; - source = (unsigned char*) glyph->bitmap.buffer; - for (bitmap_y = 0; bitmap_y < glyph->bitmap.rows; bitmap_y++) { + source = (unsigned char*) bitmap.buffer; + for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) { if (horizontal_dir) { - yy = bitmap_y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); - yy -= PIXEL(glyph_info[i].y_offset); + yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender); + yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2; } else { - yy = bitmap_y + PIXEL(y + glyph->metrics.vertBearingY) + ascender; + yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender; yy += PIXEL(glyph_info[i].y_offset); } if (yy >= 0 && yy < im->ysize) { @@ -900,12 +949,13 @@ font_render(FontObject* self, PyObject* args) } } } - source += glyph->bitmap.pitch; + source += bitmap.pitch; } x += glyph_info[i].x_advance; y -= glyph_info[i].y_advance; } + FT_Stroker_Done(stroker); PyMem_Del(glyph_info); Py_RETURN_NONE; } From 27d6fc7bc52c08e9909a6a9ce3c02a48603d7968 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Aug 2019 21:35:17 +1000 Subject: [PATCH 06/54] Improved HSV conversion --- Tests/test_image_convert.py | 1 + src/libImaging/Convert.c | 180 ++++++++++++++++++++++++++++-------- 2 files changed, 145 insertions(+), 36 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 96ecf8996c9..abbd2a45f1d 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -23,6 +23,7 @@ def convert(im, mode): "RGBX", "CMYK", "YCbCr", + "HSV", ) for mode in modes: diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 5df48fb23fe..b31eb1ecc2e 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -101,6 +101,19 @@ bit2ycbcr(UINT8* out, const UINT8* in, int xsize) } } +static void +bit2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = (*in++ != 0) ? 255 : 0; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + /* ----------------- */ /* RGB/L conversions */ /* ----------------- */ @@ -175,6 +188,19 @@ l2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +l2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + UINT8 v = *in++; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = 255; + } +} + static void la2l(UINT8* out, const UINT8* in, int xsize) { @@ -196,6 +222,19 @@ la2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +la2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + UINT8 v = in[0]; + out[0] = 0; + out[1] = 0; + out[2] = v; + out[3] = in[3]; + } +} + static void rgb2bit(UINT8* out, const UINT8* in, int xsize) { @@ -283,54 +322,58 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize) } static void -rgb2hsv(UINT8* out, const UINT8* in, int xsize) +rgb2hsv_row(UINT8* out, const UINT8* in) { // following colorsys.py float h,s,rc,gc,bc,cr; UINT8 maxc,minc; UINT8 r, g, b; UINT8 uh,us,uv; - int x; - for (x = 0; x < xsize; x++, in += 4) { - r = in[0]; - g = in[1]; - b = in[2]; - - maxc = MAX(r,MAX(g,b)); - minc = MIN(r,MIN(g,b)); - uv = maxc; - if (minc == maxc){ - *out++ = 0; - *out++ = 0; - *out++ = uv; + r = in[0]; + g = in[1]; + b = in[2]; + maxc = MAX(r,MAX(g,b)); + minc = MIN(r,MIN(g,b)); + uv = maxc; + if (minc == maxc){ + uh = 0; + us = 0; + } else { + cr = (float)(maxc-minc); + s = cr/(float)maxc; + rc = ((float)(maxc-r))/cr; + gc = ((float)(maxc-g))/cr; + bc = ((float)(maxc-b))/cr; + if (r == maxc) { + h = bc-gc; + } else if (g == maxc) { + h = 2.0 + rc-bc; } else { - cr = (float)(maxc-minc); - s = cr/(float)maxc; - rc = ((float)(maxc-r))/cr; - gc = ((float)(maxc-g))/cr; - bc = ((float)(maxc-b))/cr; - if (r == maxc) { - h = bc-gc; - } else if (g == maxc) { - h = 2.0 + rc-bc; - } else { - h = 4.0 + gc-rc; - } - // incorrect hue happens if h/6 is negative. - h = fmod((h/6.0 + 1.0), 1.0); - - uh = (UINT8)CLIP8((int)(h*255.0)); - us = (UINT8)CLIP8((int)(s*255.0)); + h = 4.0 + gc-rc; + } + // incorrect hue happens if h/6 is negative. + h = fmod((h/6.0 + 1.0), 1.0); - *out++ = uh; - *out++ = us; - *out++ = uv; + uh = (UINT8)CLIP8((int)(h*255.0)); + us = (UINT8)CLIP8((int)(s*255.0)); + } + out[0] = uh; + out[1] = us; + out[2] = uv; +} - } - *out++ = in[3]; +static void +rgb2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + rgb2hsv_row(out, in); + out[3] = in[3]; } } + + static void hsv2rgb(UINT8* out, const UINT8* in, int xsize) { // following colorsys.py @@ -562,6 +605,22 @@ cmyk2rgb(UINT8* out, const UINT8* in, int xsize) } } +static void +cmyk2hsv(UINT8* out, const UINT8* in, int xsize) +{ + int x, nk, tmp; + for (x = 0; x < xsize; x++) { + nk = 255 - in[3]; + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); + rgb2hsv_row(out, out); + out[3] = 255; + out += 4; + in += 4; + } +} + /* ------------- */ /* I conversions */ /* ------------- */ @@ -631,6 +690,24 @@ i2rgb(UINT8* out, const UINT8* in_, int xsize) } } +static void +i2hsv(UINT8* out, const UINT8* in_, int xsize) +{ + int x; + INT32* in = (INT32*) in_; + for (x = 0; x < xsize; x++, in++, out+=4) { + out[0] = 0; + out[1] = 0; + if (*in <= 0) + out[2] = 0; + else if (*in >= 255) + out[2] = 255; + else + out[2] = (UINT8) *in; + out[3] = 255; + } +} + /* ------------- */ /* F conversions */ /* ------------- */ @@ -861,6 +938,7 @@ static struct { { "1", "RGBX", bit2rgb }, { "1", "CMYK", bit2cmyk }, { "1", "YCbCr", bit2ycbcr }, + { "1", "HSV", bit2hsv }, { "L", "1", l2bit }, { "L", "LA", l2la }, @@ -871,6 +949,7 @@ static struct { { "L", "RGBX", l2rgb }, { "L", "CMYK", l2cmyk }, { "L", "YCbCr", l2ycbcr }, + { "L", "HSV", l2hsv }, { "LA", "L", la2l }, { "LA", "La", lA2la }, @@ -879,6 +958,7 @@ static struct { { "LA", "RGBX", la2rgb }, { "LA", "CMYK", la2cmyk }, { "LA", "YCbCr", la2ycbcr }, + { "LA", "HSV", la2hsv }, { "La", "LA", la2lA }, @@ -887,6 +967,7 @@ static struct { { "I", "RGB", i2rgb }, { "I", "RGBA", i2rgb }, { "I", "RGBX", i2rgb }, + { "I", "HSV", i2hsv }, { "F", "L", f2l }, { "F", "I", f2i }, @@ -915,6 +996,7 @@ static struct { { "RGBA", "RGBX", rgb2rgba }, { "RGBA", "CMYK", rgb2cmyk }, { "RGBA", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGBA", "HSV", rgb2hsv }, { "RGBa", "RGBA", rgba2rgbA }, @@ -926,10 +1008,12 @@ static struct { { "RGBX", "RGB", rgba2rgb }, { "RGBX", "CMYK", rgb2cmyk }, { "RGBX", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGBX", "HSV", rgb2hsv }, { "CMYK", "RGB", cmyk2rgb }, { "CMYK", "RGBA", cmyk2rgb }, { "CMYK", "RGBX", cmyk2rgb }, + { "CMYK", "HSV", cmyk2hsv }, { "YCbCr", "L", ycbcr2l }, { "YCbCr", "LA", ycbcr2la }, @@ -1101,6 +1185,28 @@ pa2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) } } +static void +p2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, out += 4) { + const UINT8* rgb = &palette[*in++ * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + +static void +pa2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + const UINT8* rgb = &palette[in[0] * 4]; + rgb2hsv_row(out, rgb); + out[3] = 255; + } +} + static void p2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -1192,6 +1298,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) convert = alpha ? pa2cmyk : p2cmyk; else if (strcmp(mode, "YCbCr") == 0) convert = alpha ? pa2ycbcr : p2ycbcr; + else if (strcmp(mode, "HSV") == 0) + convert = alpha ? pa2hsv : p2hsv; else return (Imaging) ImagingError_ValueError("conversion not supported"); From 8fff9a24446f4ff47f6d05e4a1ed7500b29985f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Aug 2019 20:03:38 +1000 Subject: [PATCH 07/54] Fixed arc drawing bug for a non-whole number of degrees --- .../imagedraw_arc_width_non_whole_angle.png | Bin 0 -> 439 bytes Tests/test_imagedraw.py | 12 ++++++++++++ src/libImaging/Draw.c | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Tests/images/imagedraw_arc_width_non_whole_angle.png diff --git a/Tests/images/imagedraw_arc_width_non_whole_angle.png b/Tests/images/imagedraw_arc_width_non_whole_angle.png new file mode 100644 index 0000000000000000000000000000000000000000..1fb9a3c8695f96479685664b796f42b488a9852d GIT binary patch literal 439 zcmeAS@N?(olHy`uVBq!ia0vp^DImtIBm-?gPsv&b@Jh2dD)K49-=~yBb{aetX#uLCM&xL$jt{eClrh zWc89?>Kbd;{3~4ZQtNeY(DA~Iv`>ad|6DWtdj7>EyNj+t%}KjX&V9Ff$+4p)!5>Uy zW=&bz_I1meXKJaHiEpP*(VYJ9%Mu;UHODvIQqVljk$rv1nkShS;Z+g0r%Y{aH~qU} z&$Q@B>#0*WrtfrV+xlH<%av2Na(JaqUufMR!*RM1PAuGUUGe1I*M(Q4=l|l1Q^>z_ zVnfxwP3-5oY)>ni>-_cdku|hflc#N1X7}=RKxE9#Df?cJ*LKle|xT^d@u>hP|q+0ye% zmru_B^ Date: Sat, 10 Aug 2019 05:51:10 +1000 Subject: [PATCH 08/54] Do not allow floodfill to extend into negative coordinates --- .../imagedraw_floodfill_not_negative.png | Bin 0 -> 214 bytes Tests/test_imagedraw.py | 18 ++++++++++++++++++ src/PIL/ImageDraw.py | 5 +++-- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 Tests/images/imagedraw_floodfill_not_negative.png diff --git a/Tests/images/imagedraw_floodfill_not_negative.png b/Tests/images/imagedraw_floodfill_not_negative.png new file mode 100644 index 0000000000000000000000000000000000000000..c3f34a174c01635e493e365e46c7720798f6a798 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^DIm<4D~A literal 0 HcmV?d00001 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ffe35a4fa4f..e7612a9d0df 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -559,6 +559,24 @@ def test_floodfill_thresh(self): # Assert self.assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png")) + def test_floodfill_not_negative(self): + # floodfill() is experimental + # Test that floodfill does not extend into negative coordinates + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.line((W / 2, 0, W / 2, H / 2), fill="green") + draw.line((0, H / 2, W / 2, H / 2), fill="green") + + # Act + ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red")) + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_floodfill_not_negative.png") + ) + def create_base_image_draw( self, size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY ): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c9b2773881a..1805f86e42c 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -437,8 +437,9 @@ def floodfill(image, xy, value, border=None, thresh=0): new_edge = set() for (x, y) in edge: # 4 adjacent method for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): - if (s, t) in full_edge: - continue # if already processed, skip + # If already processed, or if a coordinate is negative, skip + if (s, t) in full_edge or s < 0 or t < 0: + continue try: p = pixel[s, t] except (ValueError, IndexError): From 8696f06fbe1b2a6b30fb5e64284bbb75055e5044 Mon Sep 17 00:00:00 2001 From: djy0 Date: Fri, 2 Aug 2019 08:47:38 +0800 Subject: [PATCH 09/54] Update test_file_gif.py --- Tests/test_file_gif.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2ba370c3f94..56cb9f5d506 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -472,7 +472,7 @@ def test_multiple_duration(self): except EOFError: pass - def test_identical_frames(self): + def test_partially_identical_frames(self): duration_list = [1000, 1500, 2000, 4000] out = self.tempfile("temp.gif") @@ -495,6 +495,37 @@ def test_identical_frames(self): # Assert that the new duration is the total of the identical frames self.assertEqual(reread.info["duration"], 4500) + def test_totally_identical_frames(self): + duration_list = [1000, 1500, 2000, 4000] + + out = self.tempfile("temp.gif") + + image_path = "Tests/images/bc7-argb-8bpp_MipMaps-1.png" + im_list = [ + Image.open(image_path), + Image.open(image_path), + Image.open(image_path), + Image.open(image_path), + ] + mask = Image.new("RGBA", im_list[0].size, (255, 255, 255, 0)) + + frames = [] + for image in im_list: + frames.append(Image.alpha_composite(mask, image)) + + # duration as list + frames[0].save(out, + save_all=True, append_images=frames[1:], optimize=False, duration=duration_list, loop=0, + transparency=0) + + reread = Image.open(out) + + # Assert that the first three frames were combined + self.assertEqual(reread.n_frames, 1) + + # Assert that the new duration is the total of the identical frames + self.assertEqual(reread.info["duration"], 8500) + def test_number_of_loops(self): number_of_loops = 2 From fcaf27d51cfb9f92eb46a1194ae4ad75d1d65d5b Mon Sep 17 00:00:00 2001 From: djy0 Date: Fri, 2 Aug 2019 08:54:04 +0800 Subject: [PATCH 10/54] Update GifImagePlugin.py --- src/PIL/GifImagePlugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index bbf6dc9d6b9..b5323d252e7 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -472,6 +472,10 @@ def _write_multiple_frames(im, fp, palette): else: bbox = None im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) + + # see: https://github.com/python-pillow/Pillow/issues/4002 + if len(im_frames) == 1 and 'duration' in im_frames[0]['encoderinfo']: + im.encoderinfo['duration'] = im_frames[0]['encoderinfo']['duration'] if len(im_frames) > 1: for frame_data in im_frames: From 3c971bec4197b211ff95e5022a800ac50d982f8f Mon Sep 17 00:00:00 2001 From: djy0 Date: Fri, 2 Aug 2019 09:51:27 +0800 Subject: [PATCH 11/54] format --- Tests/test_file_gif.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 56cb9f5d506..b90a7362783 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -514,10 +514,16 @@ def test_totally_identical_frames(self): frames.append(Image.alpha_composite(mask, image)) # duration as list - frames[0].save(out, - save_all=True, append_images=frames[1:], optimize=False, duration=duration_list, loop=0, - transparency=0) - + frames[0].save( + out, + save_all=True, + append_images=frames[1:], + optimize=False, + duration=duration_list, + loop=0, + transparency=0, + ) + reread = Image.open(out) # Assert that the first three frames were combined From 3499f50e5220eb6dfe2498f26036b20a99e5070c Mon Sep 17 00:00:00 2001 From: djy0 Date: Fri, 2 Aug 2019 09:54:19 +0800 Subject: [PATCH 12/54] format --- src/PIL/GifImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b5323d252e7..b359f7259bd 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -474,8 +474,8 @@ def _write_multiple_frames(im, fp, palette): im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # see: https://github.com/python-pillow/Pillow/issues/4002 - if len(im_frames) == 1 and 'duration' in im_frames[0]['encoderinfo']: - im.encoderinfo['duration'] = im_frames[0]['encoderinfo']['duration'] + if len(im_frames) == 1 and "duration" in im_frames[0]["encoderinfo"]: + im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"] if len(im_frames) > 1: for frame_data in im_frames: From 63c15dc3ba04c0cacd7a5414d3d2e6b8b9d84e73 Mon Sep 17 00:00:00 2001 From: djy0 Date: Fri, 2 Aug 2019 10:36:33 +0800 Subject: [PATCH 13/54] format --- Tests/test_file_gif.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index b90a7362783..1704c76a5d1 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -523,7 +523,6 @@ def test_totally_identical_frames(self): loop=0, transparency=0, ) - reread = Image.open(out) # Assert that the first three frames were combined From dc9c0dbfbe286cbcb33c11a0c32af5c4d45e0131 Mon Sep 17 00:00:00 2001 From: djy0 Date: Fri, 2 Aug 2019 10:37:17 +0800 Subject: [PATCH 14/54] format --- src/PIL/GifImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b359f7259bd..d2f6cd1d33d 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -472,7 +472,6 @@ def _write_multiple_frames(im, fp, palette): else: bbox = None im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) - # see: https://github.com/python-pillow/Pillow/issues/4002 if len(im_frames) == 1 and "duration" in im_frames[0]["encoderinfo"]: im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"] From 0872cb43777606ddd55cd9803694469ebb21e843 Mon Sep 17 00:00:00 2001 From: djy0 Date: Sun, 4 Aug 2019 17:32:02 +0800 Subject: [PATCH 15/54] fix comment --- Tests/test_file_gif.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1704c76a5d1..a4d021f58a1 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -525,7 +525,7 @@ def test_totally_identical_frames(self): ) reread = Image.open(out) - # Assert that the first three frames were combined + # Assert that all four frames were combined self.assertEqual(reread.n_frames, 1) # Assert that the new duration is the total of the identical frames From 88be36c27aade7af422d5bf934d05a8a4b51d7d3 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Tue, 30 Jul 2019 11:26:04 -0700 Subject: [PATCH 16/54] check prior fp result, do not use if False --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index dabcf8eb4a8..fb394af1649 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1164,7 +1164,7 @@ def _load_libtiff(self): if DEBUG: print("have getvalue. just sending in a string from getvalue") n, err = decoder.decode(self.fp.getvalue()) - elif hasattr(self.fp, "fileno"): + elif fp: # we've got a actual file on disk, pass in the fp. if DEBUG: print("have fileno, calling fileno version of the decoder.") From 597ca79b1b01f446615323c67686dd205523ba96 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Tue, 30 Jul 2019 11:28:44 -0700 Subject: [PATCH 17/54] rewind before decode, consistent with other cases --- src/PIL/TiffImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fb394af1649..bb86c53aa09 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1175,6 +1175,7 @@ def _load_libtiff(self): # we have something else. if DEBUG: print("don't have fileno or getvalue. just reading") + self.fp.seek(0) # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) From 457a97dde8232eb4b70e8704749ddd4094a4d257 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Fri, 2 Aug 2019 17:26:10 -0700 Subject: [PATCH 18/54] added test for reading TIFF from non-disk file obj --- Tests/test_file_libtiff.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index bc5003f5fc2..4ddde047a9e 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -81,6 +81,19 @@ def test_g4_tiff_bytesio(self): self.assertEqual(im.size, (500, 500)) self._assert_noerr(im) + def test_g4_non_disk_file_object(self): + """Testing loading from non-disk non-bytesio file object""" + test_file = "Tests/images/hopper_g4_500.tif" + s = io.BytesIO() + with open(test_file, "rb") as f: + s.write(f.read()) + s.seek(0) + r = io.BufferedReader(s) + im = Image.open(r) + + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(im) + def test_g4_eq_png(self): """ Checking that we're actually getting the data that we expect""" png = Image.open("Tests/images/hopper_bw_500.png") From f136187249b70c1b4bd21d23f15b0583a8960ed3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 18 Aug 2019 10:48:43 +0300 Subject: [PATCH 19/54] Remove --dist option to create deprecated bdist_wininst installers --- winbuild/build.py | 6 ++---- winbuild/build.rst | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/winbuild/build.py b/winbuild/build.py index f3121283a4f..e080736b59d 100755 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -191,16 +191,14 @@ def run_one(op): if __name__ == "__main__": - opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "dist", "wheel"]) + opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "wheel"]) opts = dict(opts) if "--clean" in opts: clean() op = "install" - if "--dist" in opts: - op = "bdist_wininst --user-access-control=auto" - elif "--wheel" in opts: + if "--wheel" in opts: op = "bdist_wheel" if "PYTHON" in os.environ: diff --git a/winbuild/build.rst b/winbuild/build.rst index a56f43d1a7c..f88024ec41d 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -79,8 +79,8 @@ Building Pillow --------------- Once the dependencies are built, run `python build.py --clean` to -build and install Pillow in virtualenvs for each python -build. `build.py --dist` will build Windows installers instead of +build and install Pillow in virtualenvs for each Python +build. `build.py --wheel` will build wheels instead of installing into virtualenvs. UNDONE -- suppressed output, what about failures. From 0b405c86beee92dec2acd2f7ad1d1f72a5ac9533 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 18 Aug 2019 23:03:43 +1000 Subject: [PATCH 20/54] Lazily use ImageFileDirectory_v1 values from Exif --- src/PIL/Image.py | 83 ++++++++++++++++++++++++-------------- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MpoImagePlugin.py | 2 +- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8b92aae45a7..d652bd978e5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3137,25 +3137,26 @@ class Exif(MutableMapping): def __init__(self): self._data = {} self._ifds = {} + self.info = None + + def _fixup(self, value): + try: + if len(value) == 1 and not isinstance(value, dict): + return value[0] + except Exception: + pass + return value def _fixup_dict(self, src_dict): # Helper function for _getexif() # returns a dict with any single item tuples/lists as individual values - def _fixup(value): - try: - if len(value) == 1 and not isinstance(value, dict): - return value[0] - except Exception: - pass - return value - - return {k: _fixup(v) for k, v in src_dict.items()} + return {k: self._fixup(v) for k, v in src_dict.items()} def _get_ifd_dict(self, tag): try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. - self.fp.seek(self._data[tag]) + self.fp.seek(self[tag]) except (KeyError, TypeError): pass else: @@ -3177,11 +3178,10 @@ def load(self, data): # process dictionary from . import TiffImagePlugin - info = TiffImagePlugin.ImageFileDirectory_v1(self.head) - self.endian = info._endian - self.fp.seek(info.next) - info.load(self.fp) - self._data = dict(self._fixup_dict(info)) + self.info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self.endian = self.info._endian + self.fp.seek(self.info.next) + self.info.load(self.fp) # get EXIF extension ifd = self._get_ifd_dict(0x8769) @@ -3189,12 +3189,6 @@ def load(self, data): self._data.update(ifd) self._ifds[0x8769] = ifd - # get gpsinfo extension - ifd = self._get_ifd_dict(0x8825) - if ifd: - self._data[0x8825] = ifd - self._ifds[0x8825] = ifd - def tobytes(self, offset=0): from . import TiffImagePlugin @@ -3203,19 +3197,20 @@ def tobytes(self, offset=0): else: head = b"MM\x00\x2A\x00\x00\x00\x08" ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) - for tag, value in self._data.items(): + for tag, value in self.items(): ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): - if tag not in self._ifds and tag in self._data: - if tag == 0xA005: # interop + if tag not in self._ifds and tag in self: + if tag in [0x8825, 0xA005]: + # gpsinfo, interop self._ifds[tag] = self._get_ifd_dict(tag) elif tag == 0x927C: # makernote from .TiffImagePlugin import ImageFileDirectory_v2 - if self._data[0x927C][:8] == b"FUJIFILM": - exif_data = self._data[0x927C] + if self[0x927C][:8] == b"FUJIFILM": + exif_data = self[0x927C] ifd_offset = i32le(exif_data[8:12]) ifd_data = exif_data[ifd_offset:] @@ -3252,8 +3247,8 @@ def get_ifd(self, tag): ImageFileDirectory_v2(), data, False ) self._ifds[0x927C] = dict(self._fixup_dict(makernote)) - elif self._data.get(0x010F) == "Nintendo": - ifd_data = self._data[0x927C] + elif self.get(0x010F) == "Nintendo": + ifd_data = self[0x927C] makernote = {} for i in range(0, struct.unpack(">H", ifd_data[:2])[0]): @@ -3291,16 +3286,29 @@ def get_ifd(self, tag): return self._ifds.get(tag, {}) def __str__(self): + if self.info is not None: + # Load all keys into self._data + for tag in self.info.keys(): + self[tag] + return str(self._data) def __len__(self): - return len(self._data) + keys = set(self._data) + if self.info is not None: + keys.update(self.info) + return len(keys) def __getitem__(self, tag): + if self.info is not None and tag not in self._data and tag in self.info: + self._data[tag] = self._fixup(self.info[tag]) + if tag == 0x8825: + self._data[tag] = self.get_ifd(tag) + del self.info[tag] return self._data[tag] def __contains__(self, tag): - return tag in self._data + return tag in self._data or (self.info is not None and tag in self.info) if not py3: @@ -3308,10 +3316,23 @@ def has_key(self, tag): return tag in self def __setitem__(self, tag, value): + if self.info is not None: + try: + del self.info[tag] + except KeyError: + pass self._data[tag] = value def __delitem__(self, tag): + if self.info is not None: + try: + del self.info[tag] + except KeyError: + pass del self._data[tag] def __iter__(self): - return iter(set(self._data)) + keys = set(self._data) + if self.info is not None: + keys.update(self.info) + return iter(keys) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index f1a2f78132a..1770fc2105b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -158,7 +158,7 @@ def APP(self, marker): # If DPI isn't in JPEG header, fetch from EXIF if "dpi" not in self.info and "exif" in self.info: try: - exif = self._getexif() + exif = self.getexif() resolution_unit = exif[0x0128] x_resolution = exif[0x011A] try: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 81b37172a18..24e953013dc 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -92,7 +92,7 @@ def seek(self, frame): n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) - exif = self._getexif() + exif = self.getexif() if 40962 in exif and 40963 in exif: self._size = (exif[40962], exif[40963]) elif "exif" in self.info: From f08a0966a089a5826b9f83329b2fee087910cdc7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2019 21:12:16 +1000 Subject: [PATCH 21/54] Corrected tag counts --- Tests/test_file_tiff_metadata.py | 6 ++++-- src/PIL/TiffTags.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 8761e431eec..c1472386463 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -239,11 +239,13 @@ def test_expty_values(self): def test_PhotoshopInfo(self): im = Image.open("Tests/images/issue_2278.tif") - self.assertIsInstance(im.tag_v2[34377], bytes) + self.assertEqual(len(im.tag_v2[34377]), 1) + self.assertIsInstance(im.tag_v2[34377][0], bytes) out = self.tempfile("temp.tiff") im.save(out) reloaded = Image.open(out) - self.assertIsInstance(reloaded.tag_v2[34377], bytes) + self.assertEqual(len(reloaded.tag_v2[34377]), 1) + self.assertIsInstance(reloaded.tag_v2[34377][0], bytes) def test_too_many_entries(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index f5a27be42e5..f53130c7263 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -175,9 +175,9 @@ def lookup(tag): 530: ("YCbCrSubSampling", SHORT, 2), 531: ("YCbCrPositioning", SHORT, 1), 532: ("ReferenceBlackWhite", RATIONAL, 6), - 700: ("XMP", BYTE, 1), + 700: ("XMP", BYTE, 0), 33432: ("Copyright", ASCII, 1), - 34377: ("PhotoshopInfo", BYTE, 1), + 34377: ("PhotoshopInfo", BYTE, 0), # FIXME add more tags here 34665: ("ExifIFD", LONG, 1), 34675: ("ICCProfile", UNDEFINED, 1), From 7a16ef16e70624dd7cb151c57314c79b8fe2fd82 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2019 20:55:41 +1000 Subject: [PATCH 22/54] Added IptcNaaInfo tag to v2 --- src/PIL/TiffTags.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index f53130c7263..82719db0ef2 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -177,6 +177,7 @@ def lookup(tag): 532: ("ReferenceBlackWhite", RATIONAL, 6), 700: ("XMP", BYTE, 0), 33432: ("Copyright", ASCII, 1), + 33723: ("IptcNaaInfo", UNDEFINED, 0), 34377: ("PhotoshopInfo", BYTE, 0), # FIXME add more tags here 34665: ("ExifIFD", LONG, 1), From 34330a7aa0b3f0585f5540c59f40690769cb22e5 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Mon, 19 Aug 2019 09:46:07 -0700 Subject: [PATCH 23/54] Update Tests/test_file_libtiff.py Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_libtiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 4ddde047a9e..6339d878acd 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -82,7 +82,7 @@ def test_g4_tiff_bytesio(self): self._assert_noerr(im) def test_g4_non_disk_file_object(self): - """Testing loading from non-disk non-bytesio file object""" + """Testing loading from non-disk non-BytesIO file object""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() with open(test_file, "rb") as f: From 4834157658e00366c2cb909fe645a0b0fb5c2a5c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 20 Aug 2019 20:42:58 +1000 Subject: [PATCH 24/54] Documented OS support for saved files [ci skip] --- src/PIL/IcoImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index c4c72e78ac4..fc728d6fbd5 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -263,6 +263,9 @@ class IcoImageFile(ImageFile.ImageFile): Handles classic, XP and Vista icon formats. + When saving, PNG compression is used. Support for this was only added in + Windows Vista. + This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis . https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki From a44e918a5b647d5eb75e0bf3fe3df4d6501cb78d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Aug 2019 19:10:00 +1000 Subject: [PATCH 25/54] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 52236c7e7b2..34405615a9b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,7 +13,7 @@ Changelog (Pillow) - Remove indirect dependencies from requirements.txt #3976 [hugovk] -- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993 +- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993, freetype to 2.10.1 #3991 [radarhere] - Change overflow check to use PY_SSIZE_T_MAX #3964 From f3ed44a5662ea2728fae2139bbdde9419356302a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2019 06:13:20 +1000 Subject: [PATCH 26/54] Changed the Image getexif method to return a shared Exif instance --- src/PIL/Image.py | 57 +++++++++++++++++++++++--------------- src/PIL/JpegImagePlugin.py | 12 +------- src/PIL/MpoImagePlugin.py | 2 -- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d652bd978e5..7c6a46fa74b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -555,6 +555,7 @@ def __init__(self): self.category = NORMAL self.readonly = 0 self.pyaccess = None + self._exif = None @property def width(self): @@ -1324,10 +1325,10 @@ def getextrema(self): return self.im.getextrema() def getexif(self): - exif = Exif() - if "exif" in self.info: - exif.load(self.info["exif"]) - return exif + if self._exif is None: + self._exif = Exif() + self._exif.load(self.info.get("exif")) + return self._exif def getim(self): """ @@ -3137,7 +3138,8 @@ class Exif(MutableMapping): def __init__(self): self._data = {} self._ifds = {} - self.info = None + self._info = None + self._loaded_exif = None def _fixup(self, value): try: @@ -3173,15 +3175,24 @@ def load(self, data): # The EXIF record consists of a TIFF file embedded in a JPEG # application marker (!). + if data == self._loaded_exif: + return + self._loaded_exif = data + self._data.clear() + self._ifds.clear() + self._info = None + if not data: + return + self.fp = io.BytesIO(data[6:]) self.head = self.fp.read(8) # process dictionary from . import TiffImagePlugin - self.info = TiffImagePlugin.ImageFileDirectory_v1(self.head) - self.endian = self.info._endian - self.fp.seek(self.info.next) - self.info.load(self.fp) + self._info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self.endian = self._info._endian + self.fp.seek(self._info.next) + self._info.load(self.fp) # get EXIF extension ifd = self._get_ifd_dict(0x8769) @@ -3286,29 +3297,29 @@ def get_ifd(self, tag): return self._ifds.get(tag, {}) def __str__(self): - if self.info is not None: + if self._info is not None: # Load all keys into self._data - for tag in self.info.keys(): + for tag in self._info.keys(): self[tag] return str(self._data) def __len__(self): keys = set(self._data) - if self.info is not None: - keys.update(self.info) + if self._info is not None: + keys.update(self._info) return len(keys) def __getitem__(self, tag): - if self.info is not None and tag not in self._data and tag in self.info: - self._data[tag] = self._fixup(self.info[tag]) + if self._info is not None and tag not in self._data and tag in self._info: + self._data[tag] = self._fixup(self._info[tag]) if tag == 0x8825: self._data[tag] = self.get_ifd(tag) - del self.info[tag] + del self._info[tag] return self._data[tag] def __contains__(self, tag): - return tag in self._data or (self.info is not None and tag in self.info) + return tag in self._data or (self._info is not None and tag in self._info) if not py3: @@ -3316,23 +3327,23 @@ def has_key(self, tag): return tag in self def __setitem__(self, tag, value): - if self.info is not None: + if self._info is not None: try: - del self.info[tag] + del self._info[tag] except KeyError: pass self._data[tag] = value def __delitem__(self, tag): - if self.info is not None: + if self._info is not None: try: - del self.info[tag] + del self._info[tag] except KeyError: pass del self._data[tag] def __iter__(self): keys = set(self._data) - if self.info is not None: - keys.update(self.info) + if self._info is not None: + keys.update(self._info) return iter(keys) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 1770fc2105b..020b952192f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -485,19 +485,9 @@ def _fixup_dict(src_dict): def _getexif(self): - # Use the cached version if possible - try: - return self.info["parsed_exif"] - except KeyError: - pass - if "exif" not in self.info: return None - exif = dict(self.getexif()) - - # Cache the result for future use - self.info["parsed_exif"] = exif - return exif + return dict(self.getexif()) def _getmp(self): diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 24e953013dc..938f2a5a646 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -86,8 +86,6 @@ def seek(self, frame): self.offset = self.__mpoffsets[frame] self.fp.seek(self.offset + 2) # skip SOI marker - if "parsed_exif" in self.info: - del self.info["parsed_exif"] if i16(self.fp.read(2)) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) From 2dbfabe6d5e553e9bf6510f8d2439bc90757309e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2019 08:10:45 +1000 Subject: [PATCH 27/54] Simplifications --- Tests/test_file_gif.py | 50 +++++++++++++-------------------------- src/PIL/GifImagePlugin.py | 8 ++++--- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index a4d021f58a1..4ff9727e1e9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -472,7 +472,7 @@ def test_multiple_duration(self): except EOFError: pass - def test_partially_identical_frames(self): + def test_identical_frames(self): duration_list = [1000, 1500, 2000, 4000] out = self.tempfile("temp.gif") @@ -495,41 +495,25 @@ def test_partially_identical_frames(self): # Assert that the new duration is the total of the identical frames self.assertEqual(reread.info["duration"], 4500) - def test_totally_identical_frames(self): - duration_list = [1000, 1500, 2000, 4000] - - out = self.tempfile("temp.gif") - - image_path = "Tests/images/bc7-argb-8bpp_MipMaps-1.png" - im_list = [ - Image.open(image_path), - Image.open(image_path), - Image.open(image_path), - Image.open(image_path), - ] - mask = Image.new("RGBA", im_list[0].size, (255, 255, 255, 0)) - - frames = [] - for image in im_list: - frames.append(Image.alpha_composite(mask, image)) + def test_identical_frames_to_single_frame(self): + for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): + out = self.tempfile("temp.gif") + im_list = [ + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + Image.new("L", (100, 100), "#000"), + ] - # duration as list - frames[0].save( - out, - save_all=True, - append_images=frames[1:], - optimize=False, - duration=duration_list, - loop=0, - transparency=0, - ) - reread = Image.open(out) + im_list[0].save( + out, save_all=True, append_images=im_list[1:], duration=duration + ) + reread = Image.open(out) - # Assert that all four frames were combined - self.assertEqual(reread.n_frames, 1) + # Assert that all frames were combined + self.assertEqual(reread.n_frames, 1) - # Assert that the new duration is the total of the identical frames - self.assertEqual(reread.info["duration"], 8500) + # Assert that the new duration is the total of the identical frames + self.assertEqual(reread.info["duration"], 8500) def test_number_of_loops(self): number_of_loops = 2 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d2f6cd1d33d..07f5ab6832d 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -472,9 +472,6 @@ def _write_multiple_frames(im, fp, palette): else: bbox = None im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) - # see: https://github.com/python-pillow/Pillow/issues/4002 - if len(im_frames) == 1 and "duration" in im_frames[0]["encoderinfo"]: - im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"] if len(im_frames) > 1: for frame_data in im_frames: @@ -492,6 +489,11 @@ def _write_multiple_frames(im, fp, palette): offset = frame_data["bbox"][:2] _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) return True + elif "duration" in im.encoderinfo and isinstance( + im.encoderinfo["duration"], (list, tuple) + ): + # Since multiple frames will not be written, add together the frame durations + im.encoderinfo["duration"] = sum(im.encoderinfo["duration"]) def _save_all(im, fp, filename): From 2df15ec7c277ffede9e92448dd4ec40af01fbc68 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2019 21:40:24 +1000 Subject: [PATCH 28/54] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 34405615a9b..f86d61b8249 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ Changelog (Pillow) - This is the last Pillow release to support Python 2.7 #3642 +- Fix bug in TIFF loading of BufferedReader #3998 + [chadawagner] + - Added fallback for finding ld on MinGW Cygwin #4019 [radarhere] From 4fef7de801ef70a7265adb19b88c549b84b0d9b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Aug 2019 20:45:52 +1000 Subject: [PATCH 29/54] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f86d61b8249..064497dcfdb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ Changelog (Pillow) - This is the last Pillow release to support Python 2.7 #3642 +- Fix bug when merging identical images to GIF with a list of durations #4003 + [djy0, radarhere] + - Fix bug in TIFF loading of BufferedReader #3998 [chadawagner] From 35a7d11f4375080f9d9fc04ad448b8d14830da96 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Aug 2019 19:02:19 +1000 Subject: [PATCH 30/54] Fixed typo [ci skip] --- Tests/test_file_tiff_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 8761e431eec..851cbed3f05 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -222,7 +222,7 @@ def test_exif_div_zero(self): self.assertEqual(0, reloaded.tag_v2[41988].numerator) self.assertEqual(0, reloaded.tag_v2[41988].denominator) - def test_expty_values(self): + def test_empty_values(self): data = io.BytesIO( b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" From 1c8aae30168bd777b87ce00f3cc0c6344f04df64 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Sep 2019 19:31:55 +1000 Subject: [PATCH 31/54] Added Tidelift Subscription link [ci skip] --- docs/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 424ccd521c5..034da6eed45 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,6 +3,8 @@ Pillow Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_. + .. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow From 2a7ad14172e8c88c4d30bb698cc569d8d07234c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Sep 2019 20:10:43 +1000 Subject: [PATCH 32/54] Added more limited support modes [ci skip] --- docs/handbook/concepts.rst | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 055821a15cf..a254d11c329 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -44,11 +44,24 @@ supports the following standard modes: * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) -PIL also provides limited support for a few special modes, including ``LA`` (L -with alpha), ``RGBX`` (true color with padding) and ``RGBa`` (true color with -premultiplied alpha). However, PIL doesn’t support user-defined modes; if you -need to handle band combinations that are not listed above, use a sequence of -Image objects. +PIL also provides limited support for a few special modes, including: + + * ``LA`` (L with alpha) + * ``PA`` (P with alpha) + * ``RGBX`` (true color with padding) + * ``RGBa`` (true color with premultiplied alpha) + * ``La`` (L with premultiplied alpha) + * ``I;16`` (16-bit unsigned integer pixels) + * ``I;16L`` (16-bit little endian unsigned integer pixels) + * ``I;16B`` (16-bit big endian unsigned integer pixels) + * ``I;16N`` (16-bit native endian unsigned integer pixels) + * ``BGR;15`` (15-bit reversed true colour) + * ``BGR;16`` (16-bit reversed true colour) + * ``BGR;24`` (24-bit reversed true colour) + * ``BGR;32`` (32-bit reversed true colour) + +However, PIL doesn’t support user-defined modes; if you need to handle band +combinations that are not listed above, use a sequence of Image objects. You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` attribute. This is a string containing one of the above values. From dea75d1210b1ca91cebf46208ba9d0d4598de442 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Sep 2019 20:11:02 +1000 Subject: [PATCH 33/54] Corrected comment --- src/libImaging/Storage.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 389089e118e..ab476939ac0 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -132,7 +132,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ - /* 15-bit true colour */ + /* 15-bit reversed true colour */ im->bands = 1; im->pixelsize = 2; im->linesize = (xsize*2 + 3) & -4; From 51457311ded46e0e5fb9a171ecbd87ac47003c86 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 6 Sep 2019 06:10:40 +1000 Subject: [PATCH 34/54] Reference Pillow not PIL [ci skip] Co-Authored-By: Hugo van Kemenade --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index a254d11c329..cb6b51e6476 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -60,7 +60,7 @@ PIL also provides limited support for a few special modes, including: * ``BGR;24`` (24-bit reversed true colour) * ``BGR;32`` (32-bit reversed true colour) -However, PIL doesn’t support user-defined modes; if you need to handle band +However, Pillow doesn’t support user-defined modes; if you need to handle band combinations that are not listed above, use a sequence of Image objects. You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` From 5ad5be4a32e26ae455a1d520b6fac0a2cef2a22f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2019 06:13:12 +1000 Subject: [PATCH 35/54] Reference Pillow not PIL [ci skip] --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index cb6b51e6476..582866345e5 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -44,7 +44,7 @@ supports the following standard modes: * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) -PIL also provides limited support for a few special modes, including: +Pillow also provides limited support for a few special modes, including: * ``LA`` (L with alpha) * ``PA`` (P with alpha) From e790a4066aa77385d13ab68eb1bbd385b9f08fd0 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 6 Sep 2019 06:18:48 +1000 Subject: [PATCH 36/54] Renamed method Co-Authored-By: Hugo van Kemenade --- src/PIL/ImageDraw.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index f51578c10f1..fd799083231 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,7 +303,7 @@ def getink(fill): return fill return ink - def drawText(ink, stroke_width=0, stroke_offset=None): + def draw_text(ink, stroke_width=0, stroke_offset=None): coord = xy try: mask, offset = font.getmask2( @@ -343,13 +343,13 @@ def drawText(ink, stroke_width=0, stroke_offset=None): if stroke_ink is not None: # Draw stroked text - drawText(stroke_ink, stroke_width) + draw_text(stroke_ink, stroke_width) # Draw normal text - drawText(ink, 0, (stroke_width, stroke_width)) + draw_text(ink, 0, (stroke_width, stroke_width)) else: # Only draw normal text - drawText(ink) + draw_text(ink) def multiline_text( self, From 9b5c6c0089252baf8523121f2c29763277c12cdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2019 20:03:16 +1000 Subject: [PATCH 37/54] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 064497dcfdb..ece635a9ff5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,18 @@ Changelog (Pillow) - This is the last Pillow release to support Python 2.7 #3642 +- Added text stroking #3978 + [radarhere, hugovk] + +- No more deprecated bdist_wininst .exe installers #4029 + [hugovk] + +- Do not allow floodfill to extend into negative coordinates #4017 + [radarhere] + +- Fixed arc drawing bug for a non-whole number of degrees #4014 + [radarhere] + - Fix bug when merging identical images to GIF with a list of durations #4003 [djy0, radarhere] From 76e5bd0f0f1c74cb00e02a2b9ff78607e4c49ca2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Sep 2019 20:07:23 +1000 Subject: [PATCH 38/54] Added brackets --- src/libImaging/Convert.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index b31eb1ecc2e..60513c66d5e 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -698,12 +698,13 @@ i2hsv(UINT8* out, const UINT8* in_, int xsize) for (x = 0; x < xsize; x++, in++, out+=4) { out[0] = 0; out[1] = 0; - if (*in <= 0) + if (*in <= 0) { out[2] = 0; - else if (*in >= 255) + } else if (*in >= 255) { out[2] = 255; - else + } else { out[2] = (UINT8) *in; + } out[3] = 255; } } From 1e3c2c9ce1aea8536e95ce74086a79793e467618 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2019 00:53:51 +1000 Subject: [PATCH 39/54] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ece635a9ff5..1aad2658852 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ Changelog (Pillow) - This is the last Pillow release to support Python 2.7 #3642 +- Improved HSV conversion #4004 + [radarhere] + - Added text stroking #3978 [radarhere, hugovk] From ef16cb8efe6a9c12034d2343783b51beb0ff9ba5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 7 Sep 2019 18:31:23 +1000 Subject: [PATCH 40/54] ImageFileDirectory_v1 does not raise KeyError --- src/PIL/Image.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7c6a46fa74b..b0ba266e0a2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3327,19 +3327,13 @@ def has_key(self, tag): return tag in self def __setitem__(self, tag, value): - if self._info is not None: - try: - del self._info[tag] - except KeyError: - pass + if self._info is not None and tag in self._info: + del self._info[tag] self._data[tag] = value def __delitem__(self, tag): - if self._info is not None: - try: - del self._info[tag] - except KeyError: - pass + if self._info is not None and tag in self._info: + del self._info[tag] del self._data[tag] def __iter__(self): From dcc90d573cdffbc5ae93327ee4d08d3b86573e47 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 7 Sep 2019 12:46:33 +0300 Subject: [PATCH 41/54] Update Tidelift badge See https://forum.tidelift.com/t/new-urls-for-project-badges/288 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ddbd12f1660..6b783a95a46 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,7 @@ To report a security vulnerability, please follow the procedure described in the .. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow -.. |tidelift| image:: https://tidelift.com/badges/github/python-pillow/Pillow?style=flat +.. |tidelift| image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=referral&utm_campaign=readme .. |version| image:: https://img.shields.io/pypi/v/pillow.svg From 5a668779e95142238fd6cd9976b6b2e9153ec9a2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 8 Sep 2019 21:27:55 +1000 Subject: [PATCH 42/54] Added tests --- Tests/test_imagefile.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 2ca5abe4ca5..f24f9deab57 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -321,3 +321,13 @@ def test_exif_interop(self): self.assertEqual( exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704} ) + + def test_exif_shared(self): + im = Image.open("Tests/images/exif.png") + exif = im.getexif() + self.assertIs(im.getexif(), exif) + + def test_exif_str(self): + im = Image.open("Tests/images/exif.png") + exif = im.getexif() + self.assertEqual(str(exif), "{274: 1}") From cce7a76f79e491040792963c32e463d9d399fe3e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2019 19:39:11 +1000 Subject: [PATCH 43/54] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1aad2658852..08e9b722529 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ Changelog (Pillow) - This is the last Pillow release to support Python 2.7 #3642 +- Lazily use ImageFileDirectory_v1 values from Exif #4031 + [radarhere] + - Improved HSV conversion #4004 [radarhere] From b913fa6a53cec30cee1811f8ed22c6ad8a055b84 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 11 Sep 2019 19:57:45 +1000 Subject: [PATCH 44/54] Increased tolerance for stroke test comparison --- Tests/test_imagedraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index b17f969b4bb..bfc2c3c9cf4 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -831,7 +831,7 @@ def test_stroke(self): # Assert self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 2.8 + im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 3.1 ) @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") From fb257ecfd98c0c8b9460f4263deaa8431ba43b45 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 31 Aug 2019 10:23:47 +0300 Subject: [PATCH 45/54] Lint using GitHub Actions --- .github/workflows/lint.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000000..60b8e2f44d9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7] + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox + + - name: Lint + run: tox -e lint From 426d0c348ff22e7ee4771ed27023ea6e328f26fc Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 2 Sep 2019 11:22:17 +0300 Subject: [PATCH 46/54] Rename var to follow example at https://github.com/actions/setup-python --- .github/workflows/lint.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 60b8e2f44d9..d5f67ce997c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,15 +8,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python: [3.7] steps: - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v1 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python }} - name: Install dependencies run: | From 77f5b04ed7cf7f725386809b70e5f5f6a84d26fe Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 2 Sep 2019 11:24:20 +0300 Subject: [PATCH 47/54] Add job name with matrix.python --- .github/workflows/lint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d5f67ce997c..4bd02b674d0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,6 +10,8 @@ jobs: matrix: python: [3.7] + name: Python ${{ matrix.python }} + steps: - uses: actions/checkout@v1 From f6eb0f77027bb105ae99d87759acbcee262bc03f Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 13 Sep 2019 08:21:24 +0300 Subject: [PATCH 48/54] Only run on push to prevent double runs --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4bd02b674d0..60459b34ac1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ name: Lint -on: [push, pull_request] +on: push jobs: build: From f792ab6c02c5ca938bfdce397079e999bdfabb9a Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 5 Sep 2019 23:49:31 +0300 Subject: [PATCH 49/54] RST uses double backticks for code (MD uses 1) --- CHANGES.rst | 18 +++++++++--------- docs/handbook/image-file-formats.rst | 8 ++++---- docs/handbook/tutorial.rst | 2 +- docs/reference/Image.rst | 6 +++--- docs/reference/ImageCms.rst | 4 ++-- docs/reference/ImagePath.rst | 2 +- docs/releasenotes/2.7.0.rst | 2 +- docs/releasenotes/2.8.0.rst | 20 +++++++++++++++----- docs/releasenotes/3.0.0.rst | 6 +++--- docs/releasenotes/3.1.0.rst | 4 ++-- winbuild/build.rst | 16 ++++++++-------- 11 files changed, 49 insertions(+), 39 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 08e9b722529..522062e5579 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -88,7 +88,7 @@ Changelog (Pillow) - Updated TIFF tile descriptors to match current decoding functionality #3795 [dmnisson] -- Added an `image.entropy()` method (second revision) #3608 +- Added an ``image.entropy()`` method (second revision) #3608 [fish2000] - Pass the correct types to PyArg_ParseTuple #3880 @@ -724,7 +724,7 @@ Changelog (Pillow) - Enable background colour parameter on rotate #3057 [storesource] -- Remove unnecessary `#if 1` directive #3072 +- Remove unnecessary ``#if 1`` directive #3072 [jdufresne] - Remove unused Python class, Path #3070 @@ -1261,7 +1261,7 @@ Changelog (Pillow) - Add decompression bomb check to Image.crop #2410 [wiredfool] -- ImageFile: Ensure that the `err_code` variable is initialized in case of exception. #2363 +- ImageFile: Ensure that the ``err_code`` variable is initialized in case of exception. #2363 [alexkiro] - Tiff: Support append_images for saving multipage TIFFs #2406 @@ -1498,7 +1498,7 @@ Changelog (Pillow) - Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360 [wiredfool] -- Prevent `nose -v` printing docstrings #2369 +- Prevent ``nose -v`` printing docstrings #2369 [hugovk] - Replaced absolute PIL imports with relative imports #2349 @@ -1943,7 +1943,7 @@ Changelog (Pillow) - Changed depends/install_*.sh urls to point to github pillow-depends repo #1983 [wiredfool] -- Allow ICC profile from `encoderinfo` while saving PNGs #1909 +- Allow ICC profile from ``encoderinfo`` while saving PNGs #1909 [homm] - Fix integer overflow on ILP32 systems (32-bit Linux). #1975 @@ -2386,7 +2386,7 @@ Changelog (Pillow) - Added PDF multipage saving #1445 [radarhere] -- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype `file` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 +- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype ``file`` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 [radarhere] - Load more broken images #1428 @@ -2878,7 +2878,7 @@ Changelog (Pillow) - Doc cleanup [wiredfool] -- Fix `ImageStat` docs #796 +- Fix ``ImageStat`` docs #796 [akx] - Added docs for ExifTags #794 @@ -3315,7 +3315,7 @@ Changelog (Pillow) - Add RGBA support to ImageColor #309 [yoavweiss] -- Test for `str`, not `"utf-8"` #306 (fixes #304) +- Test for ``str``, not ``"utf-8"`` #306 (fixes #304) [mjpieters] - Fix missing import os in _util.py #303 @@ -3421,7 +3421,7 @@ Changelog (Pillow) - Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204) -- Significant performance improvement of `alpha_composite` function #156 +- Significant performance improvement of ``alpha_composite`` function #156 [homm] - Support explicitly disabling features via --disable-* options #240 diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 023e55b8bab..3ce4ccb2bdb 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -389,12 +389,12 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: image will be saved without tiling. **quality_mode** - Either `"rates"` or `"dB"` depending on the units you want to use to + Either ``"rates"`` or ``"dB"`` depending on the units you want to use to specify image quality. **quality_layers** A sequence of numbers, each of which represents either an approximate size - reduction (if quality mode is `"rates"`) or a signal to noise ratio value + reduction (if quality mode is ``"rates"``) or a signal to noise ratio value in decibels. If not specified, defaults to a single layer of full quality. **num_resolutions** @@ -811,10 +811,10 @@ Saving sequences Support for animated WebP files will only be enabled if the system WebP library is v0.5.0 or later. You can check webp animation support at - runtime by calling `features.check("webp_anim")`. + runtime by calling ``features.check("webp_anim")``. When calling :py:meth:`~PIL.Image.Image.save`, the following options -are available when the `save_all` argument is present and true. +are available when the ``save_all`` argument is present and true. **append_images** A list of images to append as additional frames. Each of the diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index a0868a89c4b..16090b040c2 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -247,7 +247,7 @@ Transposing an image out = im.transpose(Image.ROTATE_270) ``transpose(ROTATE)`` operations can also be performed identically with -:py:meth:`~PIL.Image.Image.rotate` operations, provided the `expand` flag is +:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is true, to provide for the same changes to the image's size. A more general form of image transformations can be carried out via the diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index cfbcb8b6bac..8af56f6c1e0 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -52,7 +52,7 @@ Functions .. warning:: To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up - a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain + a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the image is over a certain limit. If desired, the warning can be turned into an error with ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging @@ -262,10 +262,10 @@ Instances of the :py:class:`Image` class have the following attributes: .. py:attribute:: filename The filename or path of the source file. Only images created with the - factory function `open` have a filename attribute. If the input is a + factory function ``open`` have a filename attribute. If the input is a file like object, the filename attribute is set to an empty string. - :type: :py:class: `string` + :type: :py:class:`string` .. py:attribute:: format diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index ea63347084e..922e1685a13 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -33,13 +33,13 @@ can be easily displayed in a chromaticity diagram, for example). .. py:attribute:: version The version number of the ICC standard that this profile follows - (e.g. `2.0`). + (e.g. ``2.0``). :type: :py:class:`float` .. py:attribute:: icc_version - Same as `version`, but in encoded format (see 7.2.4 of ICC.1:2010). + Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010). .. py:attribute:: device_class diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 978db4caff7..5ab350ef381 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -53,7 +53,7 @@ vector data. Path objects can be passed to the methods on the Converts the path to a Python list [(x, y), …]. :param flat: By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is `True`, it + [(x, y), ...]. If this argument is ``True``, it returns a flat list [x, y, ...] instead. :return: A list of coordinates. See **flat**. diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 4bb25e37104..931f9fd1e9f 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -27,7 +27,7 @@ Image resizing filters ---------------------- Image resizing methods :py:meth:`~PIL.Image.Image.resize` and -:py:meth:`~PIL.Image.Image.thumbnail` take a `resample` argument, which tells +:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells which filter should be used for resampling. Possible values are: :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.BICUBIC` and :py:attr:`PIL.Image.ANTIALIAS`. diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst index 85235d72aa9..c522fe8b0a3 100644 --- a/docs/releasenotes/2.8.0.rst +++ b/docs/releasenotes/2.8.0.rst @@ -4,18 +4,28 @@ Open HTTP response objects with Image.open ------------------------------------------ -HTTP response objects returned from `urllib2.urlopen(url)` or `requests.get(url, stream=True).raw` are 'file-like' but do not support `.seek()` operations. As a result PIL was unable to open them as images, requiring a wrap in `cStringIO` or `BytesIO`. +HTTP response objects returned from ``urllib2.urlopen(url)`` or +``requests.get(url, stream=True).raw`` are 'file-like' but do not support ``.seek()`` +operations. As a result PIL was unable to open them as images, requiring a wrap in +``cStringIO`` or ``BytesIO``. -Now new functionality has been added to `Image.open()` by way of an `.seek(0)` check and catch on exception `AttributeError` or `io.UnsupportedOperation`. If this is caught we attempt to wrap the object using `io.BytesIO` (which will only work on buffer-file-like objects). +Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and +catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we +attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like +objects). -This allows opening of files using both `urllib2` and `requests`, e.g.:: +This allows opening of files using both ``urllib2`` and ``requests``, e.g.:: Image.open(urllib2.urlopen(url)) Image.open(requests.get(url, stream=True).raw) -If the response uses content-encoding (compression, either gzip or deflate) then this will fail as both the urllib2 and requests raw file object will produce compressed data in that case. Using Content-Encoding on images is rather non-sensical as most images are already compressed, but it can still happen. +If the response uses content-encoding (compression, either gzip or deflate) then this +will fail as both the urllib2 and requests raw file object will produce compressed data +in that case. Using Content-Encoding on images is rather non-sensical as most images are +already compressed, but it can still happen. -For requests the work-around is to set the decode_content attribute on the raw object to True:: +For requests the work-around is to set the decode_content attribute on the raw object to +True:: response = requests.get(url, stream=True) response.raw.decode_content = True diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst index 9cc1de98c49..67569d3378b 100644 --- a/docs/releasenotes/3.0.0.rst +++ b/docs/releasenotes/3.0.0.rst @@ -5,8 +5,8 @@ Saving Multipage Images ----------------------- -There is now support for saving multipage images in the `GIF` and -`PDF` formats. To enable this functionality, pass in `save_all=True` +There is now support for saving multipage images in the ``GIF`` and +``PDF`` formats. To enable this functionality, pass in ``save_all=True`` as a keyword argument to the save:: im.save('test.pdf', save_all=True) @@ -37,7 +37,7 @@ have been removed in this release:: ImageDraw.setink() ImageDraw.setfill() The ImageFileIO module - The ImageFont.FreeTypeFont and ImageFont.truetype `file` keyword arg + The ImageFont.FreeTypeFont and ImageFont.truetype ``file`` keyword arg The ImagePalette private _make functions ImageWin.fromstring() ImageWin.tostring() diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 388af03acb9..3cdb6939d49 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -5,8 +5,8 @@ ImageDraw arc, chord and pieslice can now use floats ---------------------------------------------------- -There is no longer a need to ensure that the start and end arguments for `arc`, -`chord` and `pieslice` are integers. +There is no longer a need to ensure that the start and end arguments for ``arc``, +``chord`` and ``pieslice`` are integers. Note that these numbers are not simply rounded internally, but are actually utilised in the drawing process. diff --git a/winbuild/build.rst b/winbuild/build.rst index f88024ec41d..1d20840447a 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -37,8 +37,8 @@ virtualenv as well, reducing the number of packages that we need to install.) Download the rest of the Pythons by opening a command window, changing -to the `winbuild` directory, and running `python -get_pythons.py`. +to the ``winbuild`` directory, and running ``python +get_pythons.py``. UNDONE -- gpg verify the signatures (note that we can download from https) @@ -65,8 +65,8 @@ Dependencies ------------ The script 'build_dep.py' downloads and builds the dependencies. Open -a command window, change directory into `winbuild` and run `python -build_dep.py`. +a command window, change directory into ``winbuild`` and run ``python +build_dep.py``. This will download libjpeg, libtiff, libz, and freetype. It will then compile 32 and 64-bit versions of the libraries, with both versions of @@ -78,9 +78,9 @@ UNDONE -- webp, jpeg2k not recognized Building Pillow --------------- -Once the dependencies are built, run `python build.py --clean` to +Once the dependencies are built, run ``python build.py --clean`` to build and install Pillow in virtualenvs for each Python -build. `build.py --wheel` will build wheels instead of +build. ``build.py --wheel`` will build wheels instead of installing into virtualenvs. UNDONE -- suppressed output, what about failures. @@ -88,6 +88,6 @@ UNDONE -- suppressed output, what about failures. Testing Pillow -------------- -Build and install Pillow, then run `python test.py` from the -`winbuild` directory. +Build and install Pillow, then run ``python test.py`` from the +``winbuild`` directory. From 368570bfa555ca385f3f050f04a7cbe3022d6ace Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 13 Sep 2019 09:19:59 +0300 Subject: [PATCH 50/54] Revert "Only run on push to prevent double runs" This reverts commit f6eb0f77027bb105ae99d87759acbcee262bc03f. --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 60459b34ac1..4bd02b674d0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ name: Lint -on: push +on: [push, pull_request] jobs: build: From 1b70a4c6b5159135dcbfe21e5266fd9e38d949ac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Sep 2019 22:36:26 +1000 Subject: [PATCH 51/54] Use TIFF orientation --- Tests/images/g4_orientation_1.tif | Bin 0 -> 708 bytes Tests/images/g4_orientation_2.tif | Bin 0 -> 930 bytes Tests/images/g4_orientation_3.tif | Bin 0 -> 926 bytes Tests/images/g4_orientation_4.tif | Bin 0 -> 928 bytes Tests/images/g4_orientation_5.tif | Bin 0 -> 1026 bytes Tests/images/g4_orientation_6.tif | Bin 0 -> 1024 bytes Tests/images/g4_orientation_7.tif | Bin 0 -> 1022 bytes Tests/images/g4_orientation_8.tif | Bin 0 -> 1026 bytes Tests/test_file_libtiff.py | 9 +++++++++ src/PIL/TiffImagePlugin.py | 19 +++++++++++++++++++ 10 files changed, 28 insertions(+) create mode 100755 Tests/images/g4_orientation_1.tif create mode 100755 Tests/images/g4_orientation_2.tif create mode 100755 Tests/images/g4_orientation_3.tif create mode 100755 Tests/images/g4_orientation_4.tif create mode 100755 Tests/images/g4_orientation_5.tif create mode 100755 Tests/images/g4_orientation_6.tif create mode 100755 Tests/images/g4_orientation_7.tif create mode 100755 Tests/images/g4_orientation_8.tif diff --git a/Tests/images/g4_orientation_1.tif b/Tests/images/g4_orientation_1.tif new file mode 100755 index 0000000000000000000000000000000000000000..8ab0f1d0d0204488f2e12f0872a4fdb12a7c28df GIT binary patch literal 708 zcmebD)M9wT$iVRbrburoE>Aok z92kBzyk78^)c5lHtJdxR`+N86iw`|EEKLXs5$Y`T+L*mLVn2uRLMAr14gmv)lMM$H z5*jDBaWur-3{a@Me2AaJL~e%T|IAHO(F z=l=hd>+^@~7aaGpNISBfa&~09;JDdY+5HxW`OCk5U#|B2zJJO5%eD4?Q?Hu)$Cun+ z^&_79%ly{AD*yM(3uN#6xPRaO?>F{_mwt_3-+uV-!~fO4zd3c^cMqz2_1_OHJKSra$pSF_jGujhTZVQb>9U;BR~K5Xj{uwQWa-|PF$ z4_>rc7r9#hnl#Zr&b#$w!HN#IO;KX|{{OJ!=lst8Z~pzZ4|jD} z`C0qs|Kq&JdH-8hqxHWxSF8W++q#c`^{)qiYybZDUHkRht^aq+qvKv)|F!G;x4o-d zw;j55|8`<+HJi{4gDD2Pc~lOz?7xwy|0i5j$oYxRQCE-9xucR*ZhZl^S{64Uu8>gD1Q9<@AbRgd%0f!U;CdszV*=mzqO9~5ed^ctAA|&u6gj$ zJ?GX>8vL)n{t^1g^XN6p-e3DysH|q=Oql-n`dyayysQiM*8Oi0Ke?MjM`7>JiJtmv z1T|I9Hy);Fzde6T~mKL0h>wR_^+|GnQ$T($hR$JI=! z^>Hai;r|_Hp8nVTMSef$>sLSgqwnAUf6M><+9m(juU}sp|88z|^t#vg4}1R2OV`vcd%Nnj4sG-aoDpew)G%7~{jKK1R} z`nW^!%Y?aB7|e*~zTeEEWYE~e#wF0qA)&~|;o_#Gq@?`+|NjR}38zjdJWwQX;Y*03k$-srAMv##OYz_xdTnI^A6p1YcWrNHYhpN8` zv_OiH6|8p!&<)a1_68tZ21&05lnoSSFl1z6-~!S+KpY@Q0Ai3kVYFvaT4G*UW@T)1dJS+ z*gTY-tY!*?Tz_lw?hvo7eOz<>Azt>EEph^Ne@%aHd3$-^{}_a|99_yb&&ht8nHFv>$iT*$t{^JyYc&%e;;0cZ9n|~)_?X|*5(I)>;A22 zo^tIRnl=f~ z&khzT7#!v_IOLYtk*Fr&o!GJ?v0CcCw7g7BwS4B+TiKaw*G`D3Pt^PEquX-r+U0Na zKOD;DWfPN#j%VX^nRDV|GYgNPUcy0U7KMWxiTdInWn5f;z1m-Ux9Z>DukkBqYVW+E zD7q;A^}pxLK|c#zCjDLY<#OE0`zr$W-dZ=I|K*VgCndxGKNuP~7z7v?7?~Lu85kJ+ zfN{(SWk&${Oi(sRjv2~k0kYYkY#@_?3rUS2BMaCZ4xqRYlDH@mTMWtunJ*4ie-&tf z6eBBG?-HOJq@nCJK(-8$UJWQ4D9m8U$i%<}q_==LK#%~$Aa}xO&!V)%ys*sFavJ-O e8E7Xd{%ZkhfcdWn$R^~!DIg1gAOVQ6`wsx!C=o#b literal 0 HcmV?d00001 diff --git a/Tests/images/g4_orientation_4.tif b/Tests/images/g4_orientation_4.tif new file mode 100755 index 0000000000000000000000000000000000000000..166381fb73f0e0dba385f9f39893be35f89bbdb1 GIT binary patch literal 928 zcmebD)M7Zm#K7?X|NqY$4LLPhB7&lfSF}2`IGj5yl%=H5*OKATIKha8M0o)ZH0ar0(4u39$hulj!u8->dsW;p&&w6EK>+-uhQOLhBPzn|FmBwl7Bi(irc zP6hjn&h!78cwZ>oQTU^9i^Ke{6YuX|b#t9BIlg@TR^i|N`l`D1^{>|doBA^TmfzI- zxBTD#`gL!cc<9#+dn>X&T>bAPwf|lFKh31cwJVm^d%r*QFn>9BeE#c$1rg>~#DB&A zTqAGVyovwzNe|n<|Ne-xrf}4Hq`h3flILdl^%gM!pE`l~`~Nu;6}~5G|Jq;Im?2;r z#2R1v`hqT#V&Y$>fBXNmXq<3eds6*`L;gEY`}+%`SmfqAtbY0bkDmBz?%&t{#|p&n ze)Vtt|L6YaW~gnwR`UA&`qB^e?7y=Q{`XzK^=)<7?=5Td-(C8)Z{N15X1lIm&VJZ> zBT=_aD6wfOW1EC$n~;r!5@X+uM19eXZ*r+wp7y%`6HBg*LSK9Bg9axR#P}tg-2vl5)r2{?+^Thu+`+`s)39g_N+a zRcE=D6kOT=``y**+!M;L=D#)hduuKVh~_pU}R=sWME+M z1I98VlpO)&GeOxPIc6xE1;}QDvVlwnE+jRAj4WVtIDq0pNaCVMY%wSsWWG35{dJ%P zQjDx%y~}`ZkcP6?0ogK0dNrVIpfH0WBNGD`klqI306_u}gWL(DJ&V#3^TIMy%W3RC cP literal 0 HcmV?d00001 diff --git a/Tests/images/g4_orientation_5.tif b/Tests/images/g4_orientation_5.tif new file mode 100755 index 0000000000000000000000000000000000000000..9fecaad65c3f7abeaf985dc27277c385f4f387f1 GIT binary patch literal 1026 zcmebD)M8L#W?+~rC@XkP`9BCu2xa>JUwsdltx?eNO3B`eY5o73e!)|o@&!C`JXJJj zxh9a%=;+wsq@<+mHC2Dr|L@=P6|S*--}}Ue$NgW4@`39>-5vizc1p>}C_KFEH~0G@ zi^M(Z{BK{{{{8#^_y51Y>wdj^owrSPeb?@=d2%Ao0jYdkK--bDu9T9oaJ*|GYyIA( z;BbYX{OwECzkmP#{{Q#4zhCcOyPIPjFIu+N#zv!U;S@`!NA~~!E2b+&C={%`YT=t* zyLO7j2zKeF>;a}QGEapG%kdHEzly|3x;rQ6qj-P`*bXz>64 zd-H!^x_|r74I9+Aph|6g~1 z-|k(v*S2RrT6cAMd#k_|N#~}HPd@)Y%`shJRW}c0wew^daOeVw8W(p#K|ukaHG=>D z16BRY=3RAdU&oJGSF8U2|LM1*x2flad*gbrJSc{Mv4a;0%DNzc`=-%Vszy7|izc=mQJ+XriwPoYh2fTHB=ousdl0XFw91IN%0t^g{ z%nXbS3=9!K%m`)sF)=VOLD?WVW+{RFZ~5B2+Wg!- zeR2}3!$b_0!~|J&{3~%OamSA$I^dxH|Koop$_K6k&FeVV)%e*Zw#5Gb@9#GzWHY_5 zK56{_$N&F7)lwb*|NnEPd(lge7rXSj7XgW;3t$u8R);N_`Ty~WAmtTKK0dvU77-=a zR{oUT#WmmS*Xnh@;;;W-|Nr{eZChU!T+!{~EZrJ5BjC`AL;WYCCOYyLYz-Suxf$XCCw{mqYko$qz{&|~HHHm$3zJeuMneT(coX9m>&|8J`FVpotA z#0}jGwkUysb==jJYV|;e{tL^Law#~~Vs$;lr73xeg^rjy*W|?g+y6(`{d@oW|Nplq?Np^8wj1NP0D(Y@l)mLq;YBE+8!iR09GDKn!vx tjP@)_OUw()Of6@it^e47c9P=1XF&JD{PzyXCgi_wKs6wc0L0k+2LOtQPon?; literal 0 HcmV?d00001 diff --git a/Tests/images/g4_orientation_7.tif b/Tests/images/g4_orientation_7.tif new file mode 100755 index 0000000000000000000000000000000000000000..0babc91083fa17d380aa945763fbf8ab38db74d7 GIT binary patch literal 1022 zcmebD)MAihW?=Y_0TjAiOI$>_`8K?~o3|s^C!4kS|NsARdeMtrdfkg&D78;_y;#}-24#C!2W_eU@ky`amCKO@XEh?K zcV%9b{cDjQ_O<+a-TnRl|L@^fbm+gOF*8^Sge{cS^ulKfnS!1KxvHC{96@!3OJ}%`a2dA?C|NkTPVo9vH z>%|g5XOLr#xPX9l+|`v)_J4qS6~de~yc`#BeXlT5c*^G|EA+#bW%8x>e|7)A)Y}g< zd41Gf{m>0pRJ*+X&DzR!MMOfe|Ns9Jo0L~5vOCTS*zmpN@*?YB1^3pzdcW@PexP~v z_4U=>`44YI^LB0jw$_wSLc`+tCl=oHZlGC$K&}8bLpO3l_irs*xZqH`#6O3|H6MYdKC=JB!g}JhK=r-v+RWSQ>;Er( z8MIU(101xw^g131g8g`v1025l|M#E!2#m}md!T*)Ke2G_|Np!KXb6zq&e69>nL&V& zL4bjQk(q&!fq@|ch#8@5KPCnSCMX*u#|&k&0NHF%Hjv4{g``H1kp*lH2T)uHNn8|( zEe2(S%om5MHwCh#7+Jx3S%B$S8p`GYvSpC;YCzdQrAOVQ6`wsvMCR6tS literal 0 HcmV?d00001 diff --git a/Tests/images/g4_orientation_8.tif b/Tests/images/g4_orientation_8.tif new file mode 100755 index 0000000000000000000000000000000000000000..3216a37257714c434f5daec635e17474aefa19cd GIT binary patch literal 1026 zcmebD)M8L#W?-26NO_Ufk^28Y@PtFO{{OiTAm;z4e1ftr?jVtWr;6q**8~y^Pc%Fctuf9vD_|E>4lw{G>W*cD#KZ)Jz_a~+ZtcPSMFTBzV~tz!TG{~zT~ zY-iQb2xz?QzubGlL>U_`{-v*dU;p~I|NpOl(`#eHOLhlL?cZ{3m7HjcK`I}Y^8f#z zGzuI}UbV0)_%^qu?{ Date: Sat, 14 Sep 2019 21:39:58 +1000 Subject: [PATCH 52/54] Added release notes [ci skip] --- docs/releasenotes/6.2.0.rst | 67 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 68 insertions(+) create mode 100644 docs/releasenotes/6.2.0.rst diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst new file mode 100644 index 00000000000..ff82e4b8ad3 --- /dev/null +++ b/docs/releasenotes/6.2.0.rst @@ -0,0 +1,67 @@ +6.2.0 +----- + +API Additions +============= + +Text stroking +^^^^^^^^^^^^^ + +``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing +operations. They allow text to be outlined, setting the width of the stroke and +and the color respectively. If not provided, ``stroke_fill`` will default to +the ``fill`` parameter. + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 40) + font.getsize_multiline("A", stroke_width=2) + font.getsize("ABC\nAaaa", stroke_width=2) + + im = Image.new("RGB", (100, 100)) + draw = ImageDraw.Draw(im) + draw.textsize("A", font, stroke_width=2) + draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + draw.multiline_text((10, 10), "A\nB", "#f00", font, + stroke_width=2, stroke_fill="#0f0") + +API Changes +=========== + +Image.getexif +^^^^^^^^^^^^^ + +To allow for lazy loading of Exif data, ``Image.getexif()`` now returns a +shared instance of ``Image.Exif``. + +Deprecations +^^^^^^^^^^^^ + +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python +2.7, making Pillow 6.2.x the last release series to support Python 2. + +Other Changes +============= + +Removed bdist_wininst .exe installers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.exe installers fell out of favour with PEP 527, and will be deprecated in +Python 3.8. Pillow will no longer be distributing them. Wheels should be used +instead. + +Flags for libwebp in wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When building libwebp for inclusion in wheels, Pillow now adds the -O3 and +-DNDEBUG CFLAGS. These flags would be used by default if building libwebp +without debugging, and using them fixes a significant decrease in speed when +a wheel-installed copy of Pillow performs libwebp operations. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 4002888831f..76c0321e73a 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 6.2.0 6.1.0 6.0.0 5.4.1 From 28f0940d5912c032c7fa9357feb2456fe68b4bab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 Sep 2019 06:06:28 +1000 Subject: [PATCH 53/54] Added example image [ci skip] --- docs/releasenotes/6.2.0.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index ff82e4b8ad3..68d209f3519 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -28,6 +28,22 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") +For example, + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + + im = Image.new("RGB", (120, 130)) + draw = ImageDraw.Draw(im) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) + draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0") + + +creates the following image: + +.. image:: ../../Tests/images/imagedraw_stroke_different.png + API Changes =========== From 0009646a41baa6b9ec82cd1546e8e16b9148ee65 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 16 Sep 2019 21:06:13 +1000 Subject: [PATCH 54/54] Corrected comment [ci skip] --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index b0ba266e0a2..be4755e5bae 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2074,10 +2074,10 @@ def save(self, fp, format=None, **params): if open_fp: if params.get("append", False): - fp = builtins.open(filename, "r+b") - else: # Open also for reading ("+"), because TIFF save_all # writer needs to go back and edit the written data. + fp = builtins.open(filename, "r+b") + else: fp = builtins.open(filename, "w+b") try: