From 66954ad17611b3bb26091a7bd6c8b28a9f91f603 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 25 May 2020 18:51:30 +0200 Subject: [PATCH 01/42] deprecate Image.show(command="...") --- Tests/test_image.py | 12 ++++++++++++ docs/deprecations.rst | 8 ++++++++ src/PIL/Image.py | 8 +++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4d1b66dff..f284f89a7 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -664,6 +664,18 @@ class TestImage: except OSError as e: assert str(e) == "buffer overrun when reading image file" + def test_show_deprecation(self, monkeypatch): + monkeypatch.setattr(Image, "_show", lambda *args, **kwargs: None) + + im = Image.new("RGB", (50, 50), "white") + + with pytest.warns(None) as raised: + im.show() + assert not raised + + with pytest.warns(DeprecationWarning): + im.show(command="mock") + class MockEncoder: pass diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 203921c0b..508b1242b 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,6 +12,14 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. +Image.show +~~~~~~~~~~ + +.. deprecated:: 7.2.0 + +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of ``ImageShow.Viewer`` instead. + ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 8c5fff8ed..3d6da8b84 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2172,9 +2172,15 @@ class Image: :param title: Optional title to use for the image window, where possible. - :param command: command used to show the image """ + if command is not None: + warnings.warn( + "The command parameter was deprecated and will be removed in a future" + "release. Use a subclass of ImageShow.Viewer instead.", + DeprecationWarning, + ) + _show(self, title=title, command=command) def split(self): From 4a9afc79bf3011792339134896ca0fc9dfd4a46a Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 12:00:23 +0200 Subject: [PATCH 02/42] improve ImageShow docs --- docs/PIL.rst | 8 -- docs/reference/ImageShow.rst | 27 +++++ docs/reference/index.rst | 1 + src/PIL/ImageShow.py | 194 ++++++++++++++++++++--------------- 4 files changed, 140 insertions(+), 90 deletions(-) create mode 100644 docs/reference/ImageShow.rst diff --git a/docs/PIL.rst b/docs/PIL.rst index fe69fed62..21adc5a16 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -62,14 +62,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`ImageShow` Module ------------------------ - -.. automodule:: PIL.ImageShow - :members: - :undoc-members: - :show-inheritance: - :mod:`ImageTransform` Module ---------------------------- diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst new file mode 100644 index 000000000..0b012d856 --- /dev/null +++ b/docs/reference/ImageShow.rst @@ -0,0 +1,27 @@ +.. py:module:: PIL.ImageShow +.. py:currentmodule:: PIL.ImageShow + +:py:mod:`ImageShow` Module +========================== + +The :py:mod:`ImageShow` Module is used to display images. +All default viewers convert the image to be shown to PNG format. + +.. autofunction:: PIL.ImageShow.show + +.. autoclass:: WindowsViewer +.. autoclass:: MacViewer + +.. class:: UnixViewer + + The following viewers may be registered on Unix-based systems, if the given command is found: + + .. autoclass:: PIL.ImageShow.DisplayViewer + .. autoclass:: PIL.ImageShow.EogViewer + .. autoclass:: PIL.ImageShow.XVViewer + +.. autofunction:: PIL.ImageShow.register +.. autoclass:: PIL.ImageShow.Viewer + :member-order: bysource + :members: + :undoc-members: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 8c09e7b67..25c84126d 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -22,6 +22,7 @@ Reference ImagePath ImageQt ImageSequence + ImageShow ImageStat ImageTk ImageWin diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index fc5089423..4a597ce9a 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -24,6 +24,14 @@ _viewers = [] def register(viewer, order=1): + """ + The :py:func:`register` function is used to register additional viewers. + + :param viewer: The viewer to be registered. + :param order: + A negative integer to prepend this viewer to the list, + or a positive integer to append it. + """ try: if issubclass(viewer, Viewer): viewer = viewer() @@ -40,9 +48,9 @@ def show(image, title=None, **options): Display a given image. :param image: An image object. - :param title: Optional title. Not all viewers can display the title. + :param title: Optional title. Not all viewers can display the title. :param \**options: Additional viewer options. - :returns: True if a suitable viewer was found, false otherwise. + :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. """ for viewer in _viewers: if viewer.show(image, title=title, **options): @@ -56,6 +64,10 @@ class Viewer: # main api def show(self, image, **options): + """ + The main function for displaying an image. + Converts the given image to the target format and displays it. + """ # save temporary image to disk if not ( @@ -70,25 +82,31 @@ class Viewer: # hook methods format = None + """The format to convert the image into.""" options = {} + """Additional options used to convert the image.""" def get_format(self, image): - """Return format name, or None to save as PGM/PPM""" + """Return format name, or ``None`` to save as PGM/PPM.""" return self.format def get_command(self, file, **options): + """ + Returns the command used to display the file. + Not implemented in the base class. + """ raise NotImplementedError def save_image(self, image): - """Save to temporary file, and return filename""" + """Save to temporary file and return filename.""" return image._dump(format=self.get_format(image), **self.options) def show_image(self, image, **options): - """Display given image""" + """Display the given image.""" return self.show_file(self.save_image(image), **options) def show_file(self, file, **options): - """Display given file""" + """Display the given file.""" os.system(self.get_command(file, **options)) return 1 @@ -96,104 +114,116 @@ class Viewer: # -------------------------------------------------------------------- +class WindowsViewer(Viewer): + """The default viewer on Windows is the default system application for PNG files.""" + + format = "PNG" + options = {"compress_level": 1} + + def get_command(self, file, **options): + return ( + 'start "Pillow" /WAIT "%s" ' + "&& ping -n 2 127.0.0.1 >NUL " + '&& del /f "%s"' % (file, file) + ) + + if sys.platform == "win32": - - class WindowsViewer(Viewer): - format = "PNG" - options = {"compress_level": 1} - - def get_command(self, file, **options): - return ( - 'start "Pillow" /WAIT "%s" ' - "&& ping -n 2 127.0.0.1 >NUL " - '&& del /f "%s"' % (file, file) - ) - register(WindowsViewer) -elif sys.platform == "darwin": - class MacViewer(Viewer): - format = "PNG" - options = {"compress_level": 1} +class MacViewer(Viewer): + """The default viewer on MacOS using ``Preview.app``.""" - def get_command(self, file, **options): - # on darwin open returns immediately resulting in the temp - # file removal while app is opening - command = "open -a Preview.app" - command = "({} {}; sleep 20; rm -f {})&".format( - command, quote(file), quote(file) + format = "PNG" + options = {"compress_level": 1} + + def get_command(self, file, **options): + # on darwin open returns immediately resulting in the temp + # file removal while app is opening + command = "open -a Preview.app" + command = "({} {}; sleep 20; rm -f {})&".format( + command, quote(file), quote(file) + ) + return command + + def show_file(self, file, **options): + """Display given file""" + fd, path = tempfile.mkstemp() + with os.fdopen(fd, "w") as f: + f.write(file) + with open(path, "r") as f: + subprocess.Popen( + ["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"], + shell=True, + stdin=f, ) - return command + os.remove(path) + return 1 - def show_file(self, file, **options): - """Display given file""" - fd, path = tempfile.mkstemp() - with os.fdopen(fd, "w") as f: - f.write(file) - with open(path, "r") as f: - subprocess.Popen( - ["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"], - shell=True, - stdin=f, - ) - os.remove(path) - return 1 +if sys.platform == "darwin": register(MacViewer) -else: - # unixoids +class UnixViewer(Viewer): + format = "PNG" + options = {"compress_level": 1} - class UnixViewer(Viewer): - format = "PNG" - options = {"compress_level": 1} + def get_command(self, file, **options): + command = self.get_command_ex(file, **options)[0] + return "({} {}; rm -f {})&".format(command, quote(file), quote(file)) - def get_command(self, file, **options): + def show_file(self, file, **options): + """Display given file""" + fd, path = tempfile.mkstemp() + with os.fdopen(fd, "w") as f: + f.write(file) + with open(path, "r") as f: command = self.get_command_ex(file, **options)[0] - return "({} {}; rm -f {})&".format(command, quote(file), quote(file)) + subprocess.Popen( + ["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f + ) + os.remove(path) + return 1 - def show_file(self, file, **options): - """Display given file""" - fd, path = tempfile.mkstemp() - with os.fdopen(fd, "w") as f: - f.write(file) - with open(path, "r") as f: - command = self.get_command_ex(file, **options)[0] - subprocess.Popen( - ["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f - ) - os.remove(path) - return 1 - # implementations +class DisplayViewer(UnixViewer): + """The ImageMagick ``display`` command.""" - class DisplayViewer(UnixViewer): - def get_command_ex(self, file, **options): - command = executable = "display" - return command, executable + def get_command_ex(self, file, **options): + command = executable = "display" + return command, executable + +class EogViewer(UnixViewer): + """The GNOME Image Viewer ``eog`` command.""" + + def get_command_ex(self, file, **options): + command = executable = "eog" + return command, executable + + +class XVViewer(UnixViewer): + """ + The X Viewer ``xv`` command. + This viewer supports the ``title`` parameter. + """ + + def get_command_ex(self, file, title=None, **options): + # note: xv is pretty outdated. most modern systems have + # imagemagick's display command instead. + command = executable = "xv" + if title: + command += " -name %s" % quote(title) + return command, executable + + +if sys.platform not in ("win32", "darwin"): # unixoids if shutil.which("display"): register(DisplayViewer) - - class EogViewer(UnixViewer): - def get_command_ex(self, file, **options): - command = executable = "eog" - return command, executable - if shutil.which("eog"): register(EogViewer) - - class XVViewer(UnixViewer): - def get_command_ex(self, file, title=None, **options): - # note: xv is pretty outdated. most modern systems have - # imagemagick's display command instead. - command = executable = "xv" - if title: - command += " -name %s" % quote(title) - return command, executable - if shutil.which("xv"): register(XVViewer) From f19e3ec1246d28bab3092d3c53cc4b17b55cdafd Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 13:46:52 +0200 Subject: [PATCH 03/42] promote JpegPresets from autodoc section --- docs/PIL.rst | 8 -------- docs/reference/JpegPresets.rst | 11 +++++++++++ docs/reference/index.rst | 1 + src/PIL/JpegPresets.py | 9 +++++---- 4 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 docs/reference/JpegPresets.rst diff --git a/docs/PIL.rst b/docs/PIL.rst index 21adc5a16..e7939554d 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -70,14 +70,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`JpegPresets` Module -------------------------- - -.. automodule:: PIL.JpegPresets - :members: - :undoc-members: - :show-inheritance: - :mod:`PaletteFile` Module ------------------------- diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst new file mode 100644 index 000000000..bf93f891f --- /dev/null +++ b/docs/reference/JpegPresets.rst @@ -0,0 +1,11 @@ +.. py:currentmodule:: PIL.JpegPresets + +:py:mod:`JpegPresets` Module +============================ + +.. automodule:: PIL.JpegPresets + + .. data:: presets + :annotation: + + A dict of all supported presets. diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 25c84126d..9f9e7142b 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -28,6 +28,7 @@ Reference ImageWin ExifTags TiffTags + JpegPresets PSDraw PixelAccess PyAccess diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 118dab061..09691d79d 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -1,9 +1,11 @@ """ JPEG quality settings equivalent to the Photoshop settings. +Can be used when saving JPEG files. -More presets can be added to the presets dict if needed. - -Can be use when saving JPEG file. +The following presets are available by default: +``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``, +``low``, ``medium``, ``high``, ``maximum``. +More presets can be added to the :py:data:`presets` dict if needed. To apply the preset, specify:: @@ -21,7 +23,6 @@ Example:: im.save("image_name.jpg", quality="web_high") - Subsampling ----------- From d2f7e46c5dfacfa732a4e2ef11f4585737fa93ca Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 13:47:59 +0200 Subject: [PATCH 04/42] convert comments into docstrings in autodoc files --- src/PIL/BdfFontFile.py | 14 ++++++-------- src/PIL/ContainerIO.py | 8 +++++--- src/PIL/FontFile.py | 7 ++----- src/PIL/GdImageFile.py | 28 ++++++++++++++++------------ src/PIL/GimpGradientFile.py | 21 +++++++++++---------- src/PIL/GimpPaletteFile.py | 4 +--- src/PIL/ImageDraw2.py | 4 ++++ src/PIL/PaletteFile.py | 4 +--- src/PIL/PcfFontFile.py | 5 +---- src/PIL/TarIO.py | 6 ++---- src/PIL/WalImageFile.py | 17 ++++++++++------- src/PIL/_binary.py | 4 ++++ 12 files changed, 63 insertions(+), 59 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 7a485cf80..102b72e1d 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -17,13 +17,13 @@ # See the README file for information on usage and redistribution. # +""" +Parse X Bitmap Distribution Format (BDF) +""" + from . import FontFile, Image -# -------------------------------------------------------------------- -# parse X Bitmap Distribution Format (BDF) -# -------------------------------------------------------------------- - bdf_slant = { "R": "Roman", "I": "Italic", @@ -78,11 +78,9 @@ def bdf_char(f): return id, int(props["ENCODING"]), bbox, im -## -# Font file plugin for the X11 BDF format. - - class BdfFontFile(FontFile.FontFile): + """Font file plugin for the X11 BDF format.""" + def __init__(self, fp): super().__init__() diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 5bb0086f6..45e80b39a 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -14,14 +14,16 @@ # See the README file for information on usage and redistribution. # -## -# A file object that provides read access to a part of an existing -# file (for example a TAR file). import io class ContainerIO: + """ + A file object that provides read access to a part of an existing + file (for example a TAR file). + """ + def __init__(self, file, offset, length): """ Create file object. diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 979a1e33c..4243b28b6 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -23,18 +23,15 @@ WIDTH = 800 def puti16(fp, values): - # write network order (big-endian) 16-bit sequence + """write network order (big-endian) 16-bit sequence""" for v in values: if v < 0: v += 65536 fp.write(_binary.o16be(v)) -## -# Base class for raster font file handlers. - - class FontFile: + """Base class for raster font file handlers.""" bitmap = None diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index b3ab01a4e..abdc05f90 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -14,26 +14,30 @@ # -# NOTE: This format cannot be automatically recognized, so the -# class is not registered for use with Image.open(). To open a -# gd file, use the GdImageFile.open() function instead. +""" +.. note:: + This format cannot be automatically recognized, so the + class is not registered for use with :py:func:`PIL.Image.open()`. To open a + gd file, use the :py:func:`PIL.GdImageFile.open()` function instead. -# THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This -# implementation is provided for convenience and demonstrational -# purposes only. +.. warning:: + THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This + implementation is provided for convenience and demonstrational + purposes only. +""" from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i8, i16be as i16, i32be as i32 -## -# Image plugin for the GD uncompressed format. Note that this format -# is not supported by the standard Image.open function. To use -# this plugin, you have to import the GdImageFile module and -# use the GdImageFile.open function. - class GdImageFile(ImageFile.ImageFile): + """ + Image plugin for the GD uncompressed format. Note that this format + is not supported by the standard :py:func:`PIL.Image.open()` function. To use + this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and + use the :py:func:`PIL.GdImageFile.open()` function. + """ format = "GD" format_description = "GD uncompressed images" diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index 1cacf5718..7ab7f9990 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -13,17 +13,19 @@ # See the README file for information on usage and redistribution. # +""" +Stuff to translate curve segments to palette values (derived from +the corresponding code in GIMP, written by Federico Mena Quintero. +See the GIMP distribution for more information.) +""" + + from math import log, pi, sin, sqrt from ._binary import o8 -# -------------------------------------------------------------------- -# Stuff to translate curve segments to palette values (derived from -# the corresponding code in GIMP, written by Federico Mena Quintero. -# See the GIMP distribution for more information.) -# - EPSILON = 1e-10 +"""""" # Enable auto-doc for data member def linear(middle, pos): @@ -58,6 +60,7 @@ def sphere_decreasing(middle, pos): SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] +"""""" # Enable auto-doc for data member class GradientFile: @@ -98,11 +101,9 @@ class GradientFile: return b"".join(palette), "RGBA" -## -# File handler for GIMP's gradient format. - - class GimpGradientFile(GradientFile): + """File handler for GIMP's gradient format.""" + def __init__(self, fp): if fp.readline()[:13] != b"GIMP Gradient": diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index e3060ab8a..10fd3ad81 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -18,11 +18,9 @@ import re from ._binary import o8 -## -# File handler for GIMP's palette format. - class GimpPaletteFile: + """File handler for GIMP's palette format.""" rawmode = "RGB" diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 20b5fe4c4..f0b4698f3 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -16,6 +16,10 @@ # See the README file for information on usage and redistribution. # + +"""WCK-style drawing interface operations""" + + from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 73f1b4b27..6ccaa1f53 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -15,11 +15,9 @@ from ._binary import o8 -## -# File handler for Teragon-style palette files. - class PaletteFile: + """File handler for Teragon-style palette files.""" rawmode = "RGB" diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index c463533cd..f8836ad88 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -48,11 +48,8 @@ def sz(s, o): return s[o : s.index(b"\0", o)] -## -# Font file plugin for the X11 PCF format. - - class PcfFontFile(FontFile.FontFile): + """Font file plugin for the X11 PCF format.""" name = "name" diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index ede646453..d108362fc 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -18,12 +18,10 @@ import io from . import ContainerIO -## -# A file object that provides read access to a given member of a TAR -# file. - class TarIO(ContainerIO.ContainerIO): + """A file object that provides read access to a given member of a TAR file.""" + def __init__(self, tarfile, file): """ Create file object. diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index d5a5c8e67..b578d6981 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -12,13 +12,16 @@ # See the README file for information on usage and redistribution. # -# NOTE: This format cannot be automatically recognized, so the reader -# is not registered for use with Image.open(). To open a WAL file, use -# the WalImageFile.open() function instead. +""" +This reader is based on the specification available from: +https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml +and has been tested with a few sample files found using google. -# This reader is based on the specification available from: -# https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml -# and has been tested with a few sample files found using google. +.. note:: + This format cannot be automatically recognized, so the reader + is not registered for use with :py:func:`PIL.Image.open()`. + To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead. +""" import builtins @@ -31,7 +34,7 @@ def open(filename): Load texture from a Quake2 WAL texture file. By default, a Quake2 standard palette is attached to the texture. - To override the palette, use the putpalette method. + To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. :param filename: WAL file name, or an opened file handle. :returns: An image instance. diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index 529b8c94b..5564f450d 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -11,6 +11,10 @@ # See the README file for information on usage and redistribution. # + +"""Binary input/output support routines.""" + + from struct import pack, unpack_from From eab2260313625787c2b645f83a7a9d99165ca7af Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 14:39:48 +0200 Subject: [PATCH 05/42] add docs for ImageDraw2 based on ImageDraw, fix ImageDraw links --- docs/PIL.rst | 3 +- docs/reference/ImageDraw.rst | 38 ++++++++++---------- src/PIL/ImageDraw2.py | 70 +++++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 22 deletions(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index e7939554d..07cacf31f 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -52,13 +52,12 @@ can be found here. :undoc-members: :show-inheritance: -.. intentionally skipped documenting this because it's not documented anywhere - :mod:`ImageDraw2` Module ------------------------ .. automodule:: PIL.ImageDraw2 :members: + :member-order: bysource :undoc-members: :show-inheritance: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 782b0434b..495a7d117 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -124,7 +124,7 @@ Example: Draw Multiline Text Functions --------- -.. py:class:: PIL.ImageDraw.Draw(im, mode=None) +.. py:method:: Draw(im, mode=None) Creates an object that can be used to draw in the given image. @@ -140,13 +140,13 @@ Functions Methods ------- -.. py:method:: PIL.ImageDraw.ImageDraw.getfont() +.. py:method:: ImageDraw.getfont() Get the current default font. :returns: An image font. -.. py:method:: PIL.ImageDraw.ImageDraw.arc(xy, start, end, fill=None, width=0) +.. py:method:: ImageDraw.arc(xy, start, end, fill=None, width=0) Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. @@ -162,7 +162,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.bitmap(xy, bitmap, fill=None) +.. py:method:: ImageDraw.bitmap(xy, bitmap, fill=None) Draws a bitmap (mask) at the given position, using the current fill color for the non-zero portions. The bitmap should be a valid transparency mask @@ -173,7 +173,7 @@ Methods To paste pixel data into an image, use the :py:meth:`~PIL.Image.Image.paste` method on the image itself. -.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.chord(xy, start, end, fill=None, outline=None, width=1) Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points with a straight line. @@ -187,7 +187,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.ellipse(xy, fill=None, outline=None, width=1) Draws an ellipse inside the given bounding box. @@ -200,9 +200,9 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0, joint=None) +.. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) - Draws a line between the coordinates in the **xy** list. + Draws a line between the coordinates in the ``xy`` list. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -216,7 +216,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.pieslice(xy, start, end, fill=None, outline=None, width=1) Same as arc, but also draws straight lines between the end points and the center of the bounding box. @@ -233,7 +233,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.point(xy, fill=None) +.. py:method:: ImageDraw.point(xy, fill=None) Draws points (individual pixels) at the given coordinates. @@ -241,7 +241,7 @@ Methods numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the point. -.. py:method:: PIL.ImageDraw.ImageDraw.polygon(xy, fill=None, outline=None) +.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None) Draws a polygon. @@ -254,7 +254,7 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) Draws a rectangle. @@ -267,13 +267,13 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: PIL.ImageDraw.ImageDraw.shape(shape, fill=None, outline=None) +.. py:method:: ImageDraw.shape(shape, fill=None, outline=None) .. warning:: This method is experimental. Draw a shape. -.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None) +.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None) Draws the string at the given position. @@ -325,7 +325,7 @@ Methods .. versionadded:: 6.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None) +.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None) Draws the string at the given position. @@ -362,7 +362,7 @@ Methods .. versionadded:: 6.0.0 -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) +.. py:method:: 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. @@ -401,7 +401,7 @@ Methods .. 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) +.. py:method:: 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. @@ -439,7 +439,7 @@ Methods .. versionadded:: 6.2.0 -.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) +.. py:method:: getdraw(im=None, hints=None) .. warning:: This method is experimental. @@ -450,7 +450,7 @@ Methods :param hints: An optional list of hints. :returns: A (drawing context, drawing resource factory) tuple. -.. py:method:: PIL.ImageDraw.floodfill(image, xy, value, border=None, thresh=0) +.. py:method:: floodfill(image, xy, value, border=None, thresh=0) .. warning:: This method is experimental. diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index f0b4698f3..b14b68e3e 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -17,24 +17,34 @@ # -"""WCK-style drawing interface operations""" +""" +(Experimental) WCK-style drawing interface operations + +.. seealso:: :py:mod:`PIL.ImageDraw` +""" from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath class Pen: + """Stores an outline color and width.""" + def __init__(self, color, width=1, opacity=255): self.color = ImageColor.getrgb(color) self.width = width class Brush: + """Stores a fill color""" + def __init__(self, color, opacity=255): self.color = ImageColor.getrgb(color) class Font: + """Stores a TrueType font and color""" + def __init__(self, color, file, size=12): # FIXME: add support for bitmap fonts self.color = ImageColor.getrgb(color) @@ -42,6 +52,10 @@ class Font: class Draw: + """ + (Experimental) WCK-style drawing interface + """ + def __init__(self, image, size=None, color=None): if not hasattr(image, "im"): image = Image.new(image, size, color) @@ -77,35 +91,89 @@ class Draw: getattr(self.draw, op)(xy, fill=fill, outline=outline) def settransform(self, offset): + """Sets a transformation offset.""" (xoffset, yoffset) = offset self.transform = (1, 0, xoffset, 0, 1, yoffset) def arc(self, xy, start, end, *options): + """ + Draws an arc (a portion of a circle outline) between the start and end + angles, inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` + """ self.render("arc", xy, start, end, *options) def chord(self, xy, start, end, *options): + """ + Same as :py:meth:`~PIL.ImageDraw2.ImageDraw.arc`, but connects the end points + with a straight line. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` + """ self.render("chord", xy, start, end, *options) def ellipse(self, xy, *options): + """ + Draws an ellipse inside the given bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` + """ self.render("ellipse", xy, *options) def line(self, xy, *options): + """ + Draws a line between the coordinates in the ``xy`` list. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` + """ self.render("line", xy, *options) def pieslice(self, xy, start, end, *options): + """ + Same as arc, but also draws straight lines between the end points and the + center of the bounding box. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` + """ self.render("pieslice", xy, start, end, *options) def polygon(self, xy, *options): + """ + Draws a polygon. + + The polygon outline consists of straight lines between the given + coordinates, plus a straight line between the last and the first + coordinate. + + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` + """ self.render("polygon", xy, *options) def rectangle(self, xy, *options): + """ + Draws a rectangle. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` + """ self.render("rectangle", xy, *options) def text(self, xy, text, font): + """ + Draws the string at the given position. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` + """ if self.transform: xy = ImagePath.Path(xy) xy.transform(self.transform) self.draw.text(xy, text, font=font.font, fill=font.color) def textsize(self, text, font): + """ + Return the size of the given string, in pixels. + + .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textsize` + """ return self.draw.textsize(text, font=font.font) From eb150c5518ac25fa2e423e6ef471a00ddb1ab44d Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 14:46:14 +0200 Subject: [PATCH 06/42] sort docs index --- docs/reference/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 9f9e7142b..df7a63b04 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -7,8 +7,8 @@ Reference Image ImageChops - ImageColor ImageCms + ImageColor ImageDraw ImageEnhance ImageFile From 255bd0caef8118b909edb2a976caa8385f77c7c3 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 16:39:23 +0200 Subject: [PATCH 07/42] create docs section Internal Modules --- docs/PIL.rst | 9 ------- docs/reference/internal_design.rst | 2 +- docs/reference/internal_modules.rst | 38 +++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 docs/reference/internal_modules.rst diff --git a/docs/PIL.rst b/docs/PIL.rst index 07cacf31f..d098e1bb5 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -123,12 +123,3 @@ can be found here. :members: :undoc-members: :show-inheritance: - -:mod:`_binary` Module ---------------------- - -.. automodule:: PIL._binary - :members: - :undoc-members: - :show-inheritance: - diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index bbc9050cf..5f911db51 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -7,4 +7,4 @@ Internal Reference Docs open_files limits block_allocator - + internal_modules diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst new file mode 100644 index 000000000..7a7967d23 --- /dev/null +++ b/docs/reference/internal_modules.rst @@ -0,0 +1,38 @@ +Internal Modules +================ + +:mod:`_binary` Module +--------------------- + +.. automodule:: PIL._binary + :members: + :undoc-members: + :show-inheritance: + +:mod:`_tkinter_finder` Module +----------------------------- + +.. automodule:: PIL._tkinter_finder + :members: + :undoc-members: + :show-inheritance: + +:mod:`_util` Module +------------------- + +.. automodule:: PIL._util + :members: + :undoc-members: + :show-inheritance: + +:mod:`_version` Module +---------------------- + +.. module:: PIL._version + +.. data:: __version__ + :annotation: + :type: str + + This is the master version number for Pillow, + all other uses reference this module. From d4c432dd2f87a6ae0f6f157ca14cf22b32db6b5c Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 16:40:28 +0200 Subject: [PATCH 08/42] add autodocs for UnidentifiedImageError --- docs/PIL.rst | 8 ++++++++ src/PIL/__init__.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/docs/PIL.rst b/docs/PIL.rst index d098e1bb5..222322b0d 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -4,6 +4,14 @@ PIL Package (autodoc of remaining modules) Reference for modules whose documentation has not yet been ported or written can be found here. +:mod:`PIL` Module +------------------------- + +.. py:module:: PIL + +.. autoexception:: UnidentifiedImageError + :show-inheritance: + :mod:`BdfFontFile` Module ------------------------- diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 81b87e012..d225ed134 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -132,4 +132,8 @@ _plugins = [ class UnidentifiedImageError(OSError): + """ + Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified. + """ + pass From c40b0e54267d148fd6d1bb8523ec0f8fe8e44cf3 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 17:16:15 +0200 Subject: [PATCH 09/42] fix some docs links --- docs/reference/ExifTags.rst | 6 ++++-- docs/reference/ImageEnhance.rst | 11 ++++++----- docs/reference/ImageStat.rst | 8 ++++---- docs/reference/JpegPresets.rst | 4 ++-- docs/reference/TiffTags.rst | 19 +++++++++++-------- src/PIL/ExifTags.py | 14 ++++++-------- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 9fc7cd13b..39fdab02c 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -7,7 +7,8 @@ The :py:mod:`ExifTags` module exposes two dictionaries which provide constants and clear-text names for various well-known EXIF tags. -.. py:class:: PIL.ExifTags.TAGS +.. py:data:: TAGS + :type: dict The TAG dictionary maps 16-bit integer EXIF tag enumerations to descriptive string names. For instance: @@ -16,7 +17,8 @@ provide constants and clear-text names for various well-known EXIF tags. >>> TAGS[0x010e] 'ImageDescription' -.. py:class:: PIL.ExifTags.GPSTAGS +.. py:data:: GPSTAGS + :type: dict The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to descriptive string names. For instance: diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index b172054b2..f98d8f780 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -29,7 +29,8 @@ Classes All enhancement classes implement a common interface, containing a single method: -.. py:class:: PIL.ImageEnhance._Enhance +.. py:class:: _Enhance + .. py:method:: enhance(factor) Returns an enhanced image. @@ -40,7 +41,7 @@ method: etc), and higher values more. There are no restrictions on this value. -.. py:class:: PIL.ImageEnhance.Color(image) +.. py:class:: Color(image) Adjust image color balance. @@ -49,7 +50,7 @@ method: factor of 0.0 gives a black and white image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Contrast(image) +.. py:class:: Contrast(image) Adjust image contrast. @@ -57,7 +58,7 @@ method: to the contrast control on a TV set. An enhancement factor of 0.0 gives a solid grey image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Brightness(image) +.. py:class:: Brightness(image) Adjust image brightness. @@ -65,7 +66,7 @@ method: enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the original image. -.. py:class:: PIL.ImageEnhance.Sharpness(image) +.. py:class:: Sharpness(image) Adjust image sharpness. diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index 32f5917c1..e94c24aa4 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -7,7 +7,7 @@ The :py:mod:`ImageStat` module calculates global statistics for an image, or for a region of an image. -.. py:class:: PIL.ImageStat.Stat(image_or_list, mask=None) +.. py:class:: Stat(image_or_list, mask=None) Calculate statistics for the given image. If a mask is included, only the regions covered by that mask are included in the @@ -22,13 +22,13 @@ for a region of an image. .. note:: - This relies on the :py:meth:`~PIL.Image.histogram` method, and + This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and simply returns the low and high bins used. This is correct for images with 8 bits per channel, but fails for other modes such as - ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.getextrema` to + ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to return per-band extrema for the image. This is more correct and efficient because, for non-8-bit modes, the histogram method uses - :py:meth:`~PIL.Image.getextrema` to determine the bins used. + :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used. .. py:attribute:: count diff --git a/docs/reference/JpegPresets.rst b/docs/reference/JpegPresets.rst index bf93f891f..0a0914601 100644 --- a/docs/reference/JpegPresets.rst +++ b/docs/reference/JpegPresets.rst @@ -6,6 +6,6 @@ .. automodule:: PIL.JpegPresets .. data:: presets - :annotation: + :type: dict - A dict of all supported presets. + A dictionary of all supported presets. diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 3b261625a..4161110bd 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -10,8 +10,8 @@ metadata tag numbers, names, and type information. .. method:: lookup(tag) :param tag: Integer tag number - :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, - otherwise just populating the value and name from ``TAGS``. + :returns: Taginfo namedtuple, From the :py:data:`~PIL.TiffTags.TAGS_V2` info if possible, + otherwise just populating the value and name from :py:data:`~PIL.TiffTags.TAGS`. If the tag is not recognized, "unknown" is returned for the name .. versionadded:: 3.1.0 @@ -22,7 +22,7 @@ metadata tag numbers, names, and type information. :param value: Integer Tag Number :param name: Tag Name - :param type: Integer type from :py:attr:`PIL.TiffTags.TYPES` + :param type: Integer type from :py:data:`PIL.TiffTags.TYPES` :param length: Array length: 0 == variable, 1 == single value, n = fixed :param enum: Dict of name:integer value options for an enumeration @@ -33,15 +33,17 @@ metadata tag numbers, names, and type information. .. versionadded:: 3.0.0 -.. py:attribute:: PIL.TiffTags.TAGS_V2 +.. py:data:: PIL.TiffTags.TAGS_V2 + :type: dict The ``TAGS_V2`` dictionary maps 16-bit integer tag numbers to - :py:class:`PIL.TagTypes.TagInfo` tuples for metadata fields defined in the TIFF + :py:class:`PIL.TiffTags.TagInfo` tuples for metadata fields defined in the TIFF spec. .. versionadded:: 3.0.0 -.. py:attribute:: PIL.TiffTags.TAGS +.. py:data:: PIL.TiffTags.TAGS + :type: dict The ``TAGS`` dictionary maps 16-bit integer TIFF tag number to descriptive string names. For instance: @@ -50,10 +52,11 @@ metadata tag numbers, names, and type information. >>> TAGS[0x010e] 'ImageDescription' - This dictionary contains a superset of the tags in TAGS_V2, common + This dictionary contains a superset of the tags in :py:data:`~PIL.TiffTags.TAGS_V2`, common EXIF tags, and other well known metadata tags. -.. py:attribute:: PIL.TiffTags.TYPES +.. py:data:: PIL.TiffTags.TYPES + :type: dict The ``TYPES`` dictionary maps the TIFF type short integer to a human readable type name. diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index cecc3f246..f1c037e51 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -9,13 +9,11 @@ # See the README file for information on usage and redistribution. # -## -# This module provides constants and clear-text names for various -# well-known EXIF tags. -## +""" +This module provides constants and clear-text names for various +well-known EXIF tags. +""" -## -# Maps EXIF tags to tag names. TAGS = { # possibly incomplete @@ -280,9 +278,8 @@ TAGS = { 0xC74E: "OpcodeList3", 0xC761: "NoiseProfile", } +"""Maps EXIF tags to tag names.""" -## -# Maps EXIF GPS tags to tag names. GPSTAGS = { 0: "GPSVersionID", @@ -318,3 +315,4 @@ GPSTAGS = { 30: "GPSDifferential", 31: "GPSHPositioningError", } +"""Maps EXIF GPS tags to tag names.""" From 12cd02bd2de96bf1b1d6e91b9de5bfe70d5985ca Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 18:21:06 +0200 Subject: [PATCH 10/42] use xfail for failing tests --- .ci/test.sh | 2 +- .github/workflows/test-windows.yml | 2 +- Tests/test_image_resample.py | 14 +++++++------- Tests/test_imagefont.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.ci/test.sh b/.ci/test.sh index 516581ff0..b0e65abc4 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -2,7 +2,7 @@ set -e -python -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests +python -m pytest -v -x -ra -W always --cov PIL --cov Tests --cov-report term Tests # Docs if [ "$TRAVIS_PYTHON_VERSION" == "3.8" ] && [ "$TRAVIS_CPU_ARCH" == "amd64" ]; then diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 7ae26b883..1989bc00c 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -113,7 +113,7 @@ jobs: - name: Test Pillow run: | path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% - python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests + python.exe -m pytest -vxra -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests shell: cmd - name: Prepare to upload errors diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 764a3ca49..35eae128b 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -218,7 +218,7 @@ class TestImagingCoreResampleAccuracy: assert_image_equal(im, ref) -class CoreResampleConsistencyTest: +class TestCoreResampleConsistency: def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] @@ -253,7 +253,7 @@ class CoreResampleConsistencyTest: self.run_case(self.make_case("F", 1.192093e-07)) -class CoreResampleAlphaCorrectTest: +class TestCoreResampleAlphaCorrect: def make_levels_case(self, mode): i = Image.new(mode, (256, 16)) px = i.load() @@ -274,7 +274,7 @@ class CoreResampleAlphaCorrectTest: len(used_colors), y ) - @pytest.mark.skip("Current implementation isn't precise enough") + @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_rgba(self): case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.BOX)) @@ -283,7 +283,7 @@ class CoreResampleAlphaCorrectTest: self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) - @pytest.mark.skip("Current implementation isn't precise enough") + @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_la(self): case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.BOX)) @@ -329,7 +329,7 @@ class CoreResampleAlphaCorrectTest: self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) -class CoreResamplePassesTest: +class TestCoreResamplePasses: @contextmanager def count(self, diff): count = Image.core.get_stats()["new_count"] @@ -372,7 +372,7 @@ class CoreResamplePassesTest: assert_image_similar(with_box, cropped, 0.1) -class CoreResampleCoefficientsTest: +class TestCoreResampleCoefficients: def test_reduce(self): test_color = 254 @@ -401,7 +401,7 @@ class CoreResampleCoefficientsTest: assert histogram[0x100 * 3 + 0xFF] == 0x10000 -class CoreResampleBoxTest: +class TestCoreResampleBox: def test_wrong_arguments(self): im = hopper() for resample in ( diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index bd79f08e3..65d749b14 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -443,7 +443,7 @@ class TestImageFont: with pytest.raises(UnicodeEncodeError): font.getsize("’") - @pytest.mark.skipif(is_pypy(), reason="failing on PyPy") + @pytest.mark.xfail(is_pypy(), reason="failing on PyPy with Raqm") def test_unicode_extended(self): # issue #3777 text = "A\u278A\U0001F12B" From fc92f56382d9335b942881e84c5a2645e5304e5c Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 20:08:27 +0200 Subject: [PATCH 11/42] replace skip_known_bad_test with xfail --- Tests/helper.py | 6 ------ Tests/test_file_palm.py | 11 +++-------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 7e8abc9c9..cdc5f4efe 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -165,12 +165,6 @@ def assert_tuple_approx_equal(actuals, targets, threshold, msg): assert value, msg + ": " + repr(actuals) + " != " + repr(targets) -def skip_known_bad_test(msg=None): - # Skip if PILLOW_RUN_KNOWN_BAD is not true in the environment. - if not os.environ.get("PILLOW_RUN_KNOWN_BAD", False): - pytest.skip(msg or "Known bad test") - - def skip_unless_feature(feature): reason = "%s not available" % feature return pytest.mark.skipif(not features.check(feature), reason=reason) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 38f6dccd9..25d194b62 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -2,15 +2,10 @@ import os.path import subprocess import pytest + from PIL import Image -from .helper import ( - IMCONVERT, - assert_image_equal, - hopper, - imagemagick_available, - skip_known_bad_test, -) +from .helper import IMCONVERT, assert_image_equal, hopper, imagemagick_available _roundtrip = imagemagick_available() @@ -62,13 +57,13 @@ def test_monochrome(tmp_path): roundtrip(tmp_path, mode) +@pytest.mark.xfail(reason="Palm P image is wrong") def test_p_mode(tmp_path): # Arrange mode = "P" # Act / Assert helper_save_as_palm(tmp_path, mode) - skip_known_bad_test("Palm P image is wrong") roundtrip(tmp_path, mode) From dc41a4ec21990ea45fc12e5b099a61c974ed8db3 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 14 Jun 2020 20:16:00 +0200 Subject: [PATCH 12/42] use skip_unless_feature in more tests --- Tests/test_image_reduce.py | 6 ++---- Tests/test_imagegrab.py | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index 353d0def0..0f92b87f8 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -1,7 +1,7 @@ import pytest from PIL import Image, ImageMath, ImageMode -from .helper import convert_to_comparable +from .helper import convert_to_comparable, skip_unless_feature codecs = dir(Image.core) @@ -254,9 +254,7 @@ def test_mode_F(): compare_reduce_with_box(im, factor) -@pytest.mark.skipif( - "jpeg2k_decoder" not in codecs, reason="JPEG 2000 support not available" -) +@skip_unless_feature("jpg_2000") def test_jpeg2k(): with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.reduce(2).size == (320, 240) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 82e746fda..3e1cfa87f 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -4,7 +4,7 @@ import sys import pytest from PIL import Image, ImageGrab -from .helper import assert_image +from .helper import assert_image, skip_unless_feature class TestImageGrab: @@ -22,7 +22,7 @@ class TestImageGrab: im = ImageGrab.grab(bbox=(10, 20, 50, 80)) assert_image(im, im.mode, (40, 60)) - @pytest.mark.skipif(not Image.core.HAVE_XCB, reason="requires XCB") + @skip_unless_feature("xcb") def test_grab_x11(self): try: if sys.platform not in ("win32", "darwin"): @@ -45,7 +45,7 @@ class TestImageGrab: ImageGrab.grab(xdisplay="") assert str(e.value).startswith("Pillow was built without XCB support") - @pytest.mark.skipif(not Image.core.HAVE_XCB, reason="requires XCB") + @skip_unless_feature("xcb") def test_grab_invalid_xdisplay(self): with pytest.raises(OSError) as e: ImageGrab.grab(xdisplay="error.test:0.0") From e2e8db4fe8b7a34f06bde86462dc6e2b9ef2a05f Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 15 Jun 2020 10:16:18 +0300 Subject: [PATCH 13/42] Fix isort --- Tests/test_file_palm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 25d194b62..e7afeef23 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -2,7 +2,6 @@ import os.path import subprocess import pytest - from PIL import Image from .helper import IMCONVERT, assert_image_equal, hopper, imagemagick_available From 703a9a0eb5a3bc4328d49e132b6ee1388e35dc16 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 15 Jun 2020 14:15:53 +0200 Subject: [PATCH 14/42] add pytest -ra into setup.cfg --- .ci/test.sh | 2 +- .github/workflows/test-windows.yml | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/test.sh b/.ci/test.sh index b0e65abc4..516581ff0 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -2,7 +2,7 @@ set -e -python -m pytest -v -x -ra -W always --cov PIL --cov Tests --cov-report term Tests +python -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests # Docs if [ "$TRAVIS_PYTHON_VERSION" == "3.8" ] && [ "$TRAVIS_CPU_ARCH" == "amd64" ]; then diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 1989bc00c..7ae26b883 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -113,7 +113,7 @@ jobs: - name: Test Pillow run: | path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH% - python.exe -m pytest -vxra -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests + python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests shell: cmd - name: Prepare to upload errors diff --git a/setup.cfg b/setup.cfg index 30843b847..5593c29b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,5 +9,5 @@ line_length = 88 multi_line_output = 3 [tool:pytest] -addopts = -rs +addopts = -ra testpaths = Tests From 5e8854b8dbcdd20b29356fbd7f1b99a983430953 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 15 Jun 2020 14:54:38 +0200 Subject: [PATCH 15/42] add note about overriding Image.show behaviour --- src/PIL/Image.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3d6da8b84..d75fcc6b0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2157,8 +2157,10 @@ class Image: def show(self, title=None, command=None): """ - Displays this image. This method is mainly intended for - debugging purposes. + Displays this image. This method is mainly intended for debugging purposes. + + This method calls :py:func:`PIL.ImageShow.show` internally. You can use + :py:func:`PIL.ImageShow.register` to override its default behaviour. The image is first saved to a temporary file. By default, it will be in PNG format. @@ -2170,8 +2172,7 @@ class Image: On Windows, the image is opened with the standard PNG display utility. - :param title: Optional title to use for the image window, - where possible. + :param title: Optional title to use for the image window, where possible. """ if command is not None: From f99e0b824bdef7b5ca92fda228c99f42c5bf7627 Mon Sep 17 00:00:00 2001 From: Kirill Kuzminykh Date: Thu, 18 Jun 2020 16:18:18 +0300 Subject: [PATCH 16/42] Replaced primitive "magic number" inside of JpegImagePlugin._accept() function by more correct version. --- Tests/test_file_jpeg.py | 20 ++++++++++++++++++++ src/PIL/JpegImagePlugin.py | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index afca875de..7702d35b5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -706,6 +706,26 @@ class TestFileJpeg: with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" + def test_reading_not_whole_file_for_define_it_type(self): + size = 1024 ** 2 + buffer = BytesIO(b"\xFF" * size) # Many xFF bytes + buffer.max_pos = 0 + orig_read = buffer.read + + def read(n=-1): + res = orig_read(n) + buffer.max_pos = max(buffer.max_pos, buffer.tell()) + return res + + buffer.read = read + with pytest.raises(OSError): + Image.open(buffer) + + # Only small part of file has been read. + # The upper limit of max_pos (8Kb) was chosen experimentally + # and increased approximately twice. + assert 0 < buffer.max_pos < 8 * 1024 + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 89e70f0e9..c9b83c032 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -323,7 +323,8 @@ MARKER = { def _accept(prefix): - return prefix[0:1] == b"\377" + # Magic number was taken from https://en.wikipedia.org/wiki/JPEG + return prefix[0:3] == b"\xFF\xD8\xFF" ## From 3e9068a34564f33620b2a3b753d0b198af2a662b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jun 2020 09:48:55 +1000 Subject: [PATCH 17/42] Decreased length of test image data --- Tests/test_file_jpeg.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 7702d35b5..25c1726c1 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -707,7 +707,7 @@ class TestFileJpeg: assert im.info["icc_profile"] == b"profile" def test_reading_not_whole_file_for_define_it_type(self): - size = 1024 ** 2 + size = 4097 buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer.max_pos = 0 orig_read = buffer.read @@ -721,10 +721,8 @@ class TestFileJpeg: with pytest.raises(OSError): Image.open(buffer) - # Only small part of file has been read. - # The upper limit of max_pos (8Kb) was chosen experimentally - # and increased approximately twice. - assert 0 < buffer.max_pos < 8 * 1024 + # Assert the entire file has not been read + assert 0 < buffer.max_pos < size @pytest.mark.skipif(not is_win32(), reason="Windows only") From abbc890b205dca4040a6ac144cf17997f1ab0e9e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jun 2020 09:51:48 +1000 Subject: [PATCH 18/42] Replaced OSError with more specific UnidentifiedImageError --- Tests/test_file_jpeg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 25c1726c1..9b7c4dcea 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -3,7 +3,7 @@ import re from io import BytesIO import pytest -from PIL import ExifTags, Image, ImageFile, JpegImagePlugin +from PIL import ExifTags, Image, ImageFile, JpegImagePlugin, UnidentifiedImageError from .helper import ( assert_image, @@ -718,7 +718,7 @@ class TestFileJpeg: return res buffer.read = read - with pytest.raises(OSError): + with pytest.raises(UnidentifiedImageError): Image.open(buffer) # Assert the entire file has not been read From 65742cfc9558eebb52e13087c2c06740478586d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jun 2020 09:57:51 +1000 Subject: [PATCH 19/42] Renamed test --- Tests/test_file_jpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 9b7c4dcea..5573086cb 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -706,7 +706,7 @@ class TestFileJpeg: with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" - def test_reading_not_whole_file_for_define_it_type(self): + def test_jpeg_magic_number(self): size = 4097 buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer.max_pos = 0 From 2155c16ae0e8736736b613dc700eaae7a14199d4 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 20 Jun 2020 12:00:30 +0100 Subject: [PATCH 20/42] improve warning wording Co-authored-by: Hugo van Kemenade --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d75fcc6b0..c4f8380df 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2177,7 +2177,7 @@ class Image: if command is not None: warnings.warn( - "The command parameter was deprecated and will be removed in a future" + "The command parameter is deprecated and will be removed in a future" "release. Use a subclass of ImageShow.Viewer instead.", DeprecationWarning, ) From 2f3deef8c56d7b9199dba3881f09445085328ab9 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 20 Jun 2020 13:10:10 +0200 Subject: [PATCH 21/42] update wording for #4706 --- src/PIL/ImageShow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c833ce8e6..57b7dcac7 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -29,8 +29,8 @@ def register(viewer, order=1): :param viewer: The viewer to be registered. :param order: - A negative integer to prepend this viewer to the list, - or a positive integer to append it. + Zero or a negative integer to prepend this viewer to the list, + a positive integer to append it. """ try: if issubclass(viewer, Viewer): From d728cd58754f6701ffb3487609eabe39602238f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 26 May 2020 16:38:38 +1000 Subject: [PATCH 22/42] Allow libtiff to write COLORMAP tag --- Tests/test_file_libtiff.py | 13 +++++++++++++ src/PIL/TiffImagePlugin.py | 1 - src/PIL/TiffTags.py | 1 - src/encode.c | 23 +++++++++++++++++++++-- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 855a9ab3a..9c18eca26 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -203,6 +203,7 @@ class TestFileLibTiff(LibTiffTestCase): del core_items[tag] except KeyError: pass + del core_items[320] # colormap is special, tested below # Type codes: # 2: "ascii", @@ -479,6 +480,18 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as im2: assert_image_equal(im, im2) + def test_palette_save(self, tmp_path): + im = hopper("P") + out = str(tmp_path / "temp.tif") + + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out) + TiffImagePlugin.WRITE_LIBTIFF = False + + with Image.open(out) as reloaded: + # colormap/palette tag + assert len(reloaded.tag_v2[320]) == 768 + def xtest_bw_compression_w_rgb(self, tmp_path): """ This test passes, but when running all tests causes a failure due to output on stderr from the error thrown by libtiff. We need to diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ee183ccba..6fc8cc8cf 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1524,7 +1524,6 @@ def _save(im, fp, filename): # BITSPERSAMPLE, etc), passing arrays with a different length will result in # segfaults. Block these tags until we add extra validation. blocklist = [ - COLORMAP, REFERENCEBLACKWHITE, SAMPLEFORMAT, STRIPBYTECOUNTS, diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 6cc9ff7f3..e1c1b701b 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -483,7 +483,6 @@ LIBTIFF_CORE = { 65537, } -LIBTIFF_CORE.remove(320) # Array of short, crashes LIBTIFF_CORE.remove(301) # Array of short, crashes LIBTIFF_CORE.remove(532) # Array of long, crashes diff --git a/src/encode.c b/src/encode.c index 03a39448d..d64f47d2b 100644 --- a/src/encode.c +++ b/src/encode.c @@ -671,7 +671,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) // This list also exists in TiffTags.py const int core_tags[] = { 256, 257, 258, 259, 262, 263, 266, 269, 274, 277, 278, 280, 281, 340, - 341, 282, 283, 284, 286, 287, 296, 297, 321, 338, 32995, 32998, 32996, + 341, 282, 283, 284, 286, 287, 296, 297, 320, 321, 338, 32995, 32998, 32996, 339, 32997, 330, 531, 530, 65537 }; @@ -801,7 +801,26 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) TRACE(("Setting from Tuple: %d \n", key_int)); len = PyTuple_Size(value); - if (type == TIFF_SHORT) { + if (key_int == TIFFTAG_COLORMAP) { + int stride = 256; + if (len != 768) { + PyErr_SetString(PyExc_ValueError, "Requiring 768 items for for Colormap"); + return NULL; + } + UINT16 *av; + /* malloc check ok, calloc checks for overflow */ + av = calloc(len, sizeof(UINT16)); + if (av) { + for (i=0;istate, (ttag_t) key_int, + av, + av + stride, + av + stride * 2); + free(av); + } + } else if (type == TIFF_SHORT) { UINT16 *av; /* malloc check ok, calloc checks for overflow */ av = calloc(len, sizeof(UINT16)); From 2f0d4308076d9c59662c563d9024382b3972e1cf Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Sun, 21 Jun 2020 13:13:35 +0300 Subject: [PATCH 23/42] Fix exception causes all over the codebase --- docs/example/DdsImagePlugin.py | 8 +++---- src/PIL/BlpImagePlugin.py | 4 ++-- src/PIL/BmpImagePlugin.py | 4 ++-- src/PIL/EpsImagePlugin.py | 4 ++-- src/PIL/FpxImagePlugin.py | 4 ++-- src/PIL/GdImageFile.py | 4 ++-- src/PIL/GifImagePlugin.py | 4 ++-- src/PIL/ImImagePlugin.py | 8 +++---- src/PIL/Image.py | 40 +++++++++++++++++----------------- src/PIL/ImageCms.py | 32 +++++++++++++-------------- src/PIL/ImageFile.py | 10 ++++----- src/PIL/ImageFilter.py | 4 ++-- src/PIL/ImageFont.py | 12 +++++----- src/PIL/ImageMath.py | 8 +++---- src/PIL/ImagePalette.py | 4 ++-- src/PIL/ImageSequence.py | 8 +++---- src/PIL/IptcImagePlugin.py | 4 ++-- src/PIL/JpegImagePlugin.py | 24 ++++++++++---------- src/PIL/MicImagePlugin.py | 8 +++---- src/PIL/MspImagePlugin.py | 8 +++---- src/PIL/PcxImagePlugin.py | 4 ++-- src/PIL/PngImagePlugin.py | 18 ++++++++------- src/PIL/PsdImagePlugin.py | 4 ++-- src/PIL/SpiderImagePlugin.py | 4 ++-- src/PIL/TgaImagePlugin.py | 4 ++-- src/PIL/TiffImagePlugin.py | 12 +++++----- 26 files changed, 125 insertions(+), 123 deletions(-) diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 45f63f164..1e36f093a 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -249,8 +249,8 @@ class DXT1Decoder(ImageFile.PyDecoder): def decode(self, buffer): try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) - except struct.error: - raise OSError("Truncated DDS file") + except struct.error as e: + raise OSError("Truncated DDS file") from e return 0, 0 @@ -260,8 +260,8 @@ class DXT5Decoder(ImageFile.PyDecoder): def decode(self, buffer): try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) - except struct.error: - raise OSError("Truncated DDS file") + except struct.error as e: + raise OSError("Truncated DDS file") from e return 0, 0 diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 5ccba37db..cb8a08e20 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -282,8 +282,8 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): self.magic = self.fd.read(4) self._read_blp_header() self._load() - except struct.error: - raise OSError("Truncated Blp file") + except struct.error as e: + raise OSError("Truncated Blp file") from e return 0, 0 def _read_palette(self): diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 85e2350c5..e87f7b95e 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -304,8 +304,8 @@ def _dib_save(im, fp, filename): def _save(im, fp, filename, bitmap_header=True): try: rawmode, bits, colors = SAVE[im.mode] - except KeyError: - raise OSError("cannot write mode %s as BMP" % im.mode) + except KeyError as e: + raise OSError("cannot write mode %s as BMP" % im.mode) from e info = im.encoderinfo diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index e27a57671..652dc489a 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -231,8 +231,8 @@ class EpsImageFile(ImageFile.ImageFile): try: m = split.match(s) - except re.error: - raise SyntaxError("not an EPS file") + except re.error as e: + raise SyntaxError("not an EPS file") from e if m: k, v = m.group(1, 2) diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 81501e244..bbee9e24d 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -59,8 +59,8 @@ class FpxImageFile(ImageFile.ImageFile): try: self.ole = olefile.OleFileIO(self.fp) - except OSError: - raise SyntaxError("not an FPX file; invalid OLE file") + except OSError as e: + raise SyntaxError("not an FPX file; invalid OLE file") from e if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": raise SyntaxError("not an FPX file; bad root CLSID") diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index b3ab01a4e..9ee373868 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -81,5 +81,5 @@ def open(fp, mode="r"): try: return GdImageFile(fp) - except SyntaxError: - raise UnidentifiedImageError("cannot identify this image file") + except SyntaxError as e: + raise UnidentifiedImageError("cannot identify this image file") from e diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d360beae..ac214bb29 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -130,9 +130,9 @@ class GifImageFile(ImageFile.ImageFile): for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError: + except EOFError as e: self.seek(last_frame) - raise EOFError("no more images in GIF file") + raise EOFError("no more images in GIF file") from e def _seek(self, frame): diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 8b03f35da..d940899b0 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -163,8 +163,8 @@ class ImImageFile(ImageFile.ImageFile): try: m = split.match(s) - except re.error: - raise SyntaxError("not an IM file") + except re.error as e: + raise SyntaxError("not an IM file") from e if m: @@ -341,8 +341,8 @@ def _save(im, fp, filename): try: image_type, rawmode = SAVE[im.mode] - except KeyError: - raise ValueError("Cannot save %s images as IM" % im.mode) + except KeyError as e: + raise ValueError("Cannot save %s images as IM" % im.mode) from e frames = im.encoderinfo.get("frames", 1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 9d94bce0e..210a0ff5e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -434,8 +434,8 @@ def _getdecoder(mode, decoder_name, args, extra=()): try: # get decoder decoder = getattr(core, decoder_name + "_decoder") - except AttributeError: - raise OSError("decoder %s not available" % decoder_name) + except AttributeError as e: + raise OSError("decoder %s not available" % decoder_name) from e return decoder(mode, *args + extra) @@ -457,8 +457,8 @@ def _getencoder(mode, encoder_name, args, extra=()): try: # get encoder encoder = getattr(core, encoder_name + "_encoder") - except AttributeError: - raise OSError("encoder %s not available" % encoder_name) + except AttributeError as e: + raise OSError("encoder %s not available" % encoder_name) from e return encoder(mode, *args + extra) @@ -971,10 +971,10 @@ class Image: if isinstance(t, tuple): try: t = trns_im.palette.getcolor(t) - except Exception: + except Exception as e: raise ValueError( "Couldn't allocate a palette color for transparency" - ) + ) from e trns_im.putpixel((0, 0), t) if mode in ("L", "RGB"): @@ -1027,8 +1027,8 @@ class Image: # normalize source image and try again im = self.im.convert(getmodebase(self.mode)) im = im.convert(mode, dither) - except KeyError: - raise ValueError("illegal conversion") + except KeyError as e: + raise ValueError("illegal conversion") from e new_im = self._new(im) if delete_trns: @@ -1625,16 +1625,16 @@ class Image: mode = getmodebase(self.mode) + "A" try: self.im.setmode(mode) - except (AttributeError, ValueError): + except (AttributeError, ValueError) as e: # do things the hard way im = self.im.convert(mode) if im.mode not in ("LA", "PA", "RGBA"): - raise ValueError # sanity check + raise ValueError from e # sanity check self.im = im self.pyaccess = None self.mode = self.im.mode - except (KeyError, ValueError): - raise ValueError("illegal image mode") + except (KeyError, ValueError) as e: + raise ValueError("illegal image mode") from e if self.mode in ("LA", "PA"): band = 1 @@ -2136,8 +2136,8 @@ class Image: init() try: format = EXTENSION[ext] - except KeyError: - raise ValueError("unknown file extension: {}".format(ext)) + except KeyError as e: + raise ValueError("unknown file extension: {}".format(ext)) from e if format.upper() not in SAVE: init() @@ -2238,8 +2238,8 @@ class Image: if isinstance(channel, str): try: channel = self.getbands().index(channel) - except ValueError: - raise ValueError('The image has no channel "{}"'.format(channel)) + except ValueError as e: + raise ValueError('The image has no channel "{}"'.format(channel)) from e return self._new(self.im.getband(channel)) @@ -2736,12 +2736,12 @@ def fromarray(obj, mode=None): if mode is None: try: typekey = (1, 1) + shape[2:], arr["typestr"] - except KeyError: - raise TypeError("Cannot handle this data type") + except KeyError as e: + raise TypeError("Cannot handle this data type") from e try: mode, rawmode = _fromarray_typemap[typekey] - except KeyError: - raise TypeError("Cannot handle this data type: %s, %s" % typekey) + except KeyError as e: + raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 723e7ceb7..8b97c19a1 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -369,7 +369,7 @@ def profileToProfile( else: imOut = transform.apply(im) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v return imOut @@ -393,7 +393,7 @@ def getOpenProfile(profileFilename): try: return ImageCmsProfile(profileFilename) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def buildTransform( @@ -474,7 +474,7 @@ def buildTransform( inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags ) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def buildProofTransform( @@ -585,7 +585,7 @@ def buildProofTransform( flags, ) except (OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v buildTransformFromOpenProfiles = buildTransform @@ -640,7 +640,7 @@ def applyTransform(im, transform, inPlace=False): else: imOut = transform.apply(im) except (TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v return imOut @@ -682,15 +682,15 @@ def createProfile(colorSpace, colorTemp=-1): if colorSpace == "LAB": try: colorTemp = float(colorTemp) - except (TypeError, ValueError): + except (TypeError, ValueError) as e: raise PyCMSError( 'Color temperature must be numeric, "%s" not valid' % colorTemp - ) + ) from e try: return core.createProfile(colorSpace, colorTemp) except (TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileName(profile): @@ -732,7 +732,7 @@ def getProfileName(profile): return "{} - {}\n".format(model, manufacturer) except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileInfo(profile): @@ -772,7 +772,7 @@ def getProfileInfo(profile): return "\r\n\r\n".join(arr) + "\r\n\r\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileCopyright(profile): @@ -800,7 +800,7 @@ def getProfileCopyright(profile): profile = ImageCmsProfile(profile) return (profile.profile.copyright or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileManufacturer(profile): @@ -828,7 +828,7 @@ def getProfileManufacturer(profile): profile = ImageCmsProfile(profile) return (profile.profile.manufacturer or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileModel(profile): @@ -857,7 +857,7 @@ def getProfileModel(profile): profile = ImageCmsProfile(profile) return (profile.profile.model or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getProfileDescription(profile): @@ -886,7 +886,7 @@ def getProfileDescription(profile): profile = ImageCmsProfile(profile) return (profile.profile.profile_description or "") + "\n" except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def getDefaultIntent(profile): @@ -925,7 +925,7 @@ def getDefaultIntent(profile): profile = ImageCmsProfile(profile) return profile.profile.rendering_intent except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def isIntentSupported(profile, intent, direction): @@ -976,7 +976,7 @@ def isIntentSupported(profile, intent, direction): else: return -1 except (AttributeError, OSError, TypeError, ValueError) as v: - raise PyCMSError(v) + raise PyCMSError(v) from v def versions(): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 9a780b4e0..9c9e89e3f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -122,7 +122,7 @@ class ImageFile(Image.Image): EOFError, # got header but not the first frame struct.error, ) as v: - raise SyntaxError(v) + raise SyntaxError(v) from v if not self.mode or self.size[0] <= 0: raise SyntaxError("not identified by this driver") @@ -241,12 +241,12 @@ class ImageFile(Image.Image): while True: try: s = read(self.decodermaxblock) - except (IndexError, struct.error): + except (IndexError, struct.error) as e: # truncated png/gif if LOAD_TRUNCATED_IMAGES: break else: - raise OSError("image file is truncated") + raise OSError("image file is truncated") from e if not s: # truncated jpeg if LOAD_TRUNCATED_IMAGES: @@ -505,7 +505,7 @@ def _save(im, fp, tile, bufsize=0): try: fh = fp.fileno() fp.flush() - except (AttributeError, io.UnsupportedOperation): + except (AttributeError, io.UnsupportedOperation) as e: # compress to Python file-compatible object for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) @@ -522,7 +522,7 @@ def _save(im, fp, tile, bufsize=0): if s: break if s < 0: - raise OSError("encoder error %d when writing image file" % s) + raise OSError("encoder error %d when writing image file" % s) from e e.cleanup() else: # slight speedup: compress to real file object diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 6b0f5eb37..3e61a6ca1 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -411,10 +411,10 @@ class Color3DLUT(MultibandFilter): def _check_size(size): try: _, _, _ = size - except ValueError: + except ValueError as e: raise ValueError( "Size should be either an integer or a tuple of three integers." - ) + ) from e except TypeError: size = (size, size, size) size = [int(x) for x in size] diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 79c161713..6b7368c1b 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -503,8 +503,8 @@ class FreeTypeFont: """ try: names = self.font.getvarnames() - except AttributeError: - raise NotImplementedError("FreeType 2.9.1 or greater is required") + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name): @@ -533,8 +533,8 @@ class FreeTypeFont: """ try: axes = self.font.getvaraxes() - except AttributeError: - raise NotImplementedError("FreeType 2.9.1 or greater is required") + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e for axis in axes: axis["name"] = axis["name"].replace(b"\x00", b"") return axes @@ -546,8 +546,8 @@ class FreeTypeFont: """ try: self.font.setvaraxes(axes) - except AttributeError: - raise NotImplementedError("FreeType 2.9.1 or greater is required") + except AttributeError as e: + raise NotImplementedError("FreeType 2.9.1 or greater is required") from e class TransposedFont: diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index adbb94000..9a2d0b78e 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -57,8 +57,8 @@ class _Operand: im1.load() try: op = getattr(_imagingmath, op + "_" + im1.mode) - except AttributeError: - raise TypeError("bad operand type for '%s'" % op) + except AttributeError as e: + raise TypeError("bad operand type for '%s'" % op) from e _imagingmath.unop(op, out.im.id, im1.im.id) else: # binary operation @@ -85,8 +85,8 @@ class _Operand: im2.load() try: op = getattr(_imagingmath, op + "_" + im1.mode) - except AttributeError: - raise TypeError("bad operand type for '%s'" % op) + except AttributeError as e: + raise TypeError("bad operand type for '%s'" % op) from e _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) return _Operand(out) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index e0d439c98..5dba6176f 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -97,13 +97,13 @@ class ImagePalette: if isinstance(color, tuple): try: return self.colors[color] - except KeyError: + except KeyError as e: # allocate new color slot if isinstance(self.palette, bytes): self.palette = bytearray(self.palette) index = len(self.colors) if index >= 256: - raise ValueError("cannot allocate more than 256 colors") + raise ValueError("cannot allocate more than 256 colors") from e self.colors[color] = index self.palette[index] = color[0] self.palette[index + 256] = color[1] diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 4e9f5c210..9df910a43 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -38,8 +38,8 @@ class Iterator: try: self.im.seek(ix) return self.im - except EOFError: - raise IndexError # end of sequence + except EOFError as e: + raise IndexError from e # end of sequence def __iter__(self): return self @@ -49,8 +49,8 @@ class Iterator: self.im.seek(self.position) self.position += 1 return self.im - except EOFError: - raise StopIteration + except EOFError as e: + raise StopIteration from e def all_frames(im, func=None): diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index b2f976dda..75e7b5a2a 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -118,8 +118,8 @@ class IptcImageFile(ImageFile.ImageFile): # compression try: compression = COMPRESSION[self.getint((3, 120))] - except KeyError: - raise OSError("Unknown IPTC image compression") + except KeyError as e: + raise OSError("Unknown IPTC image compression") from e # tile if tag == (8, 10): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 89e70f0e9..449d9cde7 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -503,13 +503,13 @@ def _getmp(self): file_contents.seek(info.next) info.load(file_contents) mp = dict(info) - except Exception: - raise SyntaxError("malformed MP Index (unreadable directory)") + except Exception as e: + raise SyntaxError("malformed MP Index (unreadable directory)") from e # it's an error not to have a number of images try: quant = mp[0xB001] - except KeyError: - raise SyntaxError("malformed MP Index (no number of images)") + except KeyError as e: + raise SyntaxError("malformed MP Index (no number of images)") from e # get MP entries mpentries = [] try: @@ -545,8 +545,8 @@ def _getmp(self): mpentry["Attribute"] = mpentryattr mpentries.append(mpentry) mp[0xB002] = mpentries - except KeyError: - raise SyntaxError("malformed MP Index (bad MP Entry)") + except KeyError as e: + raise SyntaxError("malformed MP Index (bad MP Entry)") from e # Next we should try and parse the individual image unique ID list; # we don't because I've never seen this actually used in a real MPO # file and so can't test it. @@ -610,8 +610,8 @@ def _save(im, fp, filename): try: rawmode = RAWMODE[im.mode] - except KeyError: - raise OSError("cannot write mode %s as JPEG" % im.mode) + except KeyError as e: + raise OSError("cannot write mode %s as JPEG" % im.mode) from e info = im.encoderinfo @@ -663,8 +663,8 @@ def _save(im, fp, filename): for line in qtables.splitlines() for num in line.split("#", 1)[0].split() ] - except ValueError: - raise ValueError("Invalid quantization table") + except ValueError as e: + raise ValueError("Invalid quantization table") from e else: qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): @@ -679,8 +679,8 @@ def _save(im, fp, filename): if len(table) != 64: raise TypeError table = array.array("B", table) - except TypeError: - raise ValueError("Invalid quantization table") + except TypeError as e: + raise ValueError("Invalid quantization table") from e else: qtables[idx] = list(table) return qtables diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 1d7af7c7a..2aed26030 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -46,8 +46,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: self.ole = olefile.OleFileIO(self.fp) - except OSError: - raise SyntaxError("not an MIC file; invalid OLE file") + except OSError as e: + raise SyntaxError("not an MIC file; invalid OLE file") from e # find ACI subfiles with Image members (maybe not the # best way to identify MIC files, but what the... ;-) @@ -77,8 +77,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): return try: filename = self.images[frame] - except IndexError: - raise EOFError("no such frame") + except IndexError as e: + raise EOFError("no such frame") from e self.fp = self.ole.openstream(filename) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 2b2937ecf..a729e7b49 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -116,8 +116,8 @@ class MspDecoder(ImageFile.PyDecoder): rowmap = struct.unpack_from( "<%dH" % (self.state.ysize), self.fd.read(self.state.ysize * 2) ) - except struct.error: - raise OSError("Truncated MSP file in row map") + except struct.error as e: + raise OSError("Truncated MSP file in row map") from e for x, rowlen in enumerate(rowmap): try: @@ -142,8 +142,8 @@ class MspDecoder(ImageFile.PyDecoder): img.write(row[idx : idx + runcount]) idx += runcount - except struct.error: - raise OSError("Corrupted MSP file in row %d" % x) + except struct.error as e: + raise OSError("Corrupted MSP file in row %d" % x) from e self.set_as_raw(img.getvalue(), ("1", 0, 1)) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 6cf10deb3..f7ae3bf70 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -131,8 +131,8 @@ def _save(im, fp, filename): try: version, bits, planes, rawmode = SAVE[im.mode] - except KeyError: - raise ValueError("Cannot save %s images as PCX" % im.mode) + except KeyError as e: + raise ValueError("Cannot save %s images as PCX" % im.mode) from e # bytes per plane stride = (im.size[0] * bits + 7) // 8 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f62bf8542..025e4ec26 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -168,8 +168,10 @@ class ChunkStream: crc2 = i32(self.fp.read(4)) if crc1 != crc2: raise SyntaxError("broken PNG file (bad header checksum in %r)" % cid) - except struct.error: - raise SyntaxError("broken PNG file (incomplete checksum in %r)" % cid) + except struct.error as e: + raise SyntaxError( + "broken PNG file (incomplete checksum in %r)" % cid + ) from e def crc_skip(self, cid, data): """Read checksum. Used if the C module is not present""" @@ -186,8 +188,8 @@ class ChunkStream: while True: try: cid, pos, length = self.read() - except struct.error: - raise OSError("truncated PNG file") + except struct.error as e: + raise OSError("truncated PNG file") from e if cid == endchunk: break @@ -737,9 +739,9 @@ class PngImageFile(ImageFile.ImageFile): for f in range(self.__frame + 1, frame + 1): try: self._seek(f) - except EOFError: + except EOFError as e: self.seek(last_frame) - raise EOFError("no more images in APNG file") + raise EOFError("no more images in APNG file") from e def _seek(self, frame, rewind=False): if frame == 0: @@ -1168,8 +1170,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): # get the corresponding PNG mode try: rawmode, mode = _OUTMODES[mode] - except KeyError: - raise OSError("cannot write mode %s as PNG" % mode) + except KeyError as e: + raise OSError("cannot write mode %s as PNG" % mode) from e # # write minimal PNG file diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 044df443d..1ff4c8624 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -144,8 +144,8 @@ class PsdImageFile(ImageFile.ImageFile): self.frame = layer self.fp = self.__fp return name, bbox - except IndexError: - raise EOFError("no such layer") + except IndexError as e: + raise EOFError("no such layer") from e def tell(self): # return layer number (0=image, 1..max=layers) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index cbd31cf82..56aac2987 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -111,8 +111,8 @@ class SpiderImageFile(ImageFile.ImageFile): hdrlen = isSpiderHeader(t) if hdrlen == 0: raise SyntaxError("not a valid Spider file") - except struct.error: - raise SyntaxError("not a valid Spider file") + except struct.error as e: + raise SyntaxError("not a valid Spider file") from e h = (99,) + t # add 1 value : spider header index starts at 1 iform = int(h[5]) diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index fd71e545d..566f0ac18 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -167,8 +167,8 @@ def _save(im, fp, filename): try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] - except KeyError: - raise OSError("cannot write mode %s as TGA" % im.mode) + except KeyError as e: + raise OSError("cannot write mode %s as TGA" % im.mode) from e if "rle" in im.encoderinfo: rle = im.encoderinfo["rle"] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index bee05e6ed..cb725d952 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1117,8 +1117,8 @@ class TiffImageFile(ImageFile.ImageFile): ) try: decoder.setimage(self.im, extents) - except ValueError: - raise OSError("Couldn't set the image") + except ValueError as e: + raise OSError("Couldn't set the image") from e close_self_fp = self._exclusive_fp and not self.is_animated if hasattr(self.fp, "getvalue"): @@ -1231,9 +1231,9 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("format key: {}".format(key)) try: self.mode, rawmode = OPEN_INFO[key] - except KeyError: + except KeyError as e: logger.debug("- unsupported format") - raise SyntaxError("unknown pixel mode") + raise SyntaxError("unknown pixel mode") from e logger.debug("- raw mode: {}".format(rawmode)) logger.debug("- pil mode: {}".format(self.mode)) @@ -1400,8 +1400,8 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] - except KeyError: - raise OSError("cannot write mode %s as TIFF" % im.mode) + except KeyError as e: + raise OSError("cannot write mode %s as TIFF" % im.mode) from e ifd = ImageFileDirectory_v2(prefix=prefix) From 29dbabd54473b584ca670d6e915b631b4fde7772 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 21 Jun 2020 11:35:47 +0100 Subject: [PATCH 24/42] improve wording Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/deprecations.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 508b1242b..08906a9b6 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,8 +12,8 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Image.show -~~~~~~~~~~ +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 7.2.0 From c15dda4308c1aa0d02c2ade7af528d2182b9a90c Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 21 Jun 2020 12:16:27 +0100 Subject: [PATCH 25/42] fix typo Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c4f8380df..051996c09 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2177,7 +2177,7 @@ class Image: if command is not None: warnings.warn( - "The command parameter is deprecated and will be removed in a future" + "The command parameter is deprecated and will be removed in a future " "release. Use a subclass of ImageShow.Viewer instead.", DeprecationWarning, ) From 66eee05a37bb5674e7a9d24d2951e71ed773b8e2 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 21 Jun 2020 18:47:30 +0100 Subject: [PATCH 26/42] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- docs/PIL.rst | 2 +- src/PIL/FontFile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/PIL.rst b/docs/PIL.rst index 222322b0d..8f8cda5fb 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -5,7 +5,7 @@ Reference for modules whose documentation has not yet been ported or written can be found here. :mod:`PIL` Module -------------------------- +----------------- .. py:module:: PIL diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 4243b28b6..3ebd90730 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -23,7 +23,7 @@ WIDTH = 800 def puti16(fp, values): - """write network order (big-endian) 16-bit sequence""" + """Write network order (big-endian) 16-bit sequence""" for v in values: if v < 0: v += 65536 From 224ef2fadd1fffefa6b2ff0b98be2595c23b4188 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 22 Jun 2020 05:11:02 +0200 Subject: [PATCH 27/42] require sphinx>=2.4 --- docs/conf.py | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index a0a3315c6..d95ec2a4a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ import sphinx_rtd_theme # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +needs_sphinx = "2.4" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom diff --git a/requirements.txt b/requirements.txt index 14f934c9c..0e0d38cdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ pyflakes pyroma pytest pytest-cov +sphinx>=2.4 sphinx-rtd-theme From 6d2fe429c2f2786f455279ad6e119614091c2806 Mon Sep 17 00:00:00 2001 From: Kirill Kuzminykh Date: Mon, 22 Jun 2020 12:20:57 +0300 Subject: [PATCH 28/42] Reformat code of ``test_file_jpeg.py`. --- Tests/test_file_jpeg.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 74ea32b0b..1801cd8ff 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -3,7 +3,14 @@ import re from io import BytesIO import pytest -from PIL import ExifTags, Image, ImageFile, JpegImagePlugin, UnidentifiedImageError, features +from PIL import ( + ExifTags, + Image, + ImageFile, + JpegImagePlugin, + UnidentifiedImageError, + features, +) from .helper import ( assert_image, From 96d1a8b41867e3459c77c190d8c01a4301d524ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jun 2020 00:25:59 +1000 Subject: [PATCH 29/42] Updated _open check to match _accept --- src/PIL/JpegImagePlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index c9b83c032..6edaee795 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -338,10 +338,11 @@ class JpegImageFile(ImageFile.ImageFile): def _open(self): - s = self.fp.read(1) + s = self.fp.read(3) - if i8(s) != 255: + if s != b"\xFF\xD8\xFF": raise SyntaxError("not a JPEG file") + s = b"\xFF" # Create attributes self.bits = self.layers = 0 From 03d4f31a3453793bfb9fc068fe1dbd61ef74a3c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jun 2020 08:14:02 +1000 Subject: [PATCH 30/42] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 96239a6e2..dad3e7d00 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.2.0 (unreleased) ------------------ +- Updated JPEG magic number #4707 + [Cykooz, radarhere] + - Change STRIPBYTECOUNTS to LONG if necessary when saving #4626 [radarhere, hugovk] From 262e2aaabb691458d630eb60b70bc8ce4e7ada3a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jun 2020 08:17:26 +1000 Subject: [PATCH 31/42] Updated harfbuzz to 2.6.8 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index beafda4a4..553ed6365 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -251,9 +251,9 @@ deps = { "libs": [r"*.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.7.zip", - "filename": "harfbuzz-2.6.7.zip", - "dir": "harfbuzz-2.6.7", + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.6.8.zip", + "filename": "harfbuzz-2.6.8.zip", + "dir": "harfbuzz-2.6.8", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From 9b6fdd719f258c5a382e2cbf5f110f172f6542bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jun 2020 17:41:13 +1000 Subject: [PATCH 32/42] Call _accept instead of duplicating code --- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/DcxImagePlugin.py | 2 +- src/PIL/FliImagePlugin.py | 4 ++-- src/PIL/GifImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PixarImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PsdImagePlugin.py | 2 +- src/PIL/SgiImagePlugin.py | 3 +-- src/PIL/SunImagePlugin.py | 2 +- 11 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 85e2350c5..a1ed80aee 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -263,7 +263,7 @@ class BmpImageFile(ImageFile.ImageFile): # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) # choke if the file does not have the required magic bytes - if head_data[0:2] != b"BM": + if not _accept(head_data[0:2]): raise SyntaxError("Not a BMP file") # read the start position of the BMP image data (u32) offset = i32(head_data[10:14]) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index a12d9195b..de21db8f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -46,7 +46,7 @@ class DcxImageFile(PcxImageFile): # Header s = self.fp.read(4) - if i32(s) != MAGIC: + if not _accept(s): raise SyntaxError("not a DCX file") # Component directory diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 989094569..f09d62ce3 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -42,9 +42,8 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) - magic = i16(s[4:6]) if not ( - magic in [0xAF11, 0xAF12] + _accept(s) and i16(s[14:16]) in [0, 3] # flags and s[20:22] == b"\x00\x00" # reserved ): @@ -60,6 +59,7 @@ class FliImageFile(ImageFile.ImageFile): # animation speed duration = i32(s[16:20]) + magic = i16(s[4:6]) if magic == 0xAF11: duration = (duration * 1000) // 70 self.info["duration"] = duration diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9d360beae..6fd6182ab 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -63,7 +63,7 @@ class GifImageFile(ImageFile.ImageFile): # Screen s = self.fp.read(13) - if s[:6] not in [b"GIF87a", b"GIF89a"]: + if not _accept(s): raise SyntaxError("not a GIF file") self.info["version"] = s[:6] diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6edaee795..f846569b2 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -340,7 +340,7 @@ class JpegImageFile(ImageFile.ImageFile): s = self.fp.read(3) - if s != b"\xFF\xD8\xFF": + if not _accept(s): raise SyntaxError("not a JPEG file") s = b"\xFF" diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 2b2937ecf..26acdd898 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -51,7 +51,7 @@ class MspImageFile(ImageFile.ImageFile): # Header s = self.fp.read(32) - if s[:4] not in [b"DanM", b"LinS"]: + if not _accept(s): raise SyntaxError("not an MSP file") # Header checksum diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 5ea32ba89..91f0314b5 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -43,7 +43,7 @@ class PixarImageFile(ImageFile.ImageFile): # assuming a 4-byte magic label s = self.fp.read(4) - if s != b"\200\350\000\000": + if not _accept(s): raise SyntaxError("not a PIXAR file") # read rest of header diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index f62bf8542..51b78c7de 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -633,7 +633,7 @@ class PngImageFile(ImageFile.ImageFile): def _open(self): - if self.fp.read(8) != _MAGIC: + if not _accept(self.fp.read(8)): raise SyntaxError("not a PNG file") self.__fp = self.fp self.__frame = 0 diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 044df443d..89e55a4ce 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -61,7 +61,7 @@ class PsdImageFile(ImageFile.ImageFile): # header s = read(26) - if s[:4] != b"8BPS" or i16(s[4:]) != 1: + if not _accept(s) or i16(s[4:]) != 1: raise SyntaxError("not a PSD file") psd_bits = i16(s[22:]) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index ddd3de379..ec9855e77 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -58,8 +58,7 @@ class SgiImageFile(ImageFile.ImageFile): headlen = 512 s = self.fp.read(headlen) - # magic number : 474 - if i16(s) != 474: + if not _accept(s): raise ValueError("Not an SGI image file") # compression : verbatim or RLE diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index fd7ca8a40..d99884293 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -53,7 +53,7 @@ class SunImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(32) - if i32(s) != 0x59A66A95: + if not _accept(s): raise SyntaxError("not an SUN raster file") offset = 32 From 6c2d575f9bf05a877a7c33e19ae9be14a1c73a71 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 23 Jun 2020 18:09:12 +1000 Subject: [PATCH 33/42] Simplified passing of data to _accept Co-authored-by: Hugo van Kemenade --- src/PIL/BmpImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index a1ed80aee..7bd5a0308 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -263,7 +263,7 @@ class BmpImageFile(ImageFile.ImageFile): # read 14 bytes: magic number, filesize, reserved, header final offset head_data = self.fp.read(14) # choke if the file does not have the required magic bytes - if not _accept(head_data[0:2]): + if not _accept(head_data): raise SyntaxError("Not a BMP file") # read the start position of the BMP image data (u32) offset = i32(head_data[10:14]) From c1fe0b4e0c4319926c3322ebf0ea60dd52b9c539 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jun 2020 19:17:00 +1000 Subject: [PATCH 34/42] Use hypot function --- .../imagedraw_wide_line_larger_than_int.png | Bin 0 -> 557 bytes Tests/test_imagedraw.py | 13 +++++++++++++ src/libImaging/Draw.c | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 Tests/images/imagedraw_wide_line_larger_than_int.png diff --git a/Tests/images/imagedraw_wide_line_larger_than_int.png b/Tests/images/imagedraw_wide_line_larger_than_int.png new file mode 100644 index 0000000000000000000000000000000000000000..68073ce48276fb22191fbe03612f32955ec9b48d GIT binary patch literal 557 zcmV+|0@D47P)Nkl!vb;ZKDS8psldkw(Ct=AYVe0mMT!lBnlEWCLQ#=@1?cr5&QnZUw{ zmlZ5Lcp1XNyq7I3Y;-94y*< zg~Fn-S2QeIdIiLynO96K+IWS Date: Tue, 23 Jun 2020 18:20:43 +1000 Subject: [PATCH 35/42] Replaced assert_image_similar with assert_image_similar_tofile --- Tests/test_imagedraw.py | 74 ++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 45 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 9aac1a0c8..56b189ecd 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -5,7 +5,7 @@ from PIL import Image, ImageColor, ImageDraw, ImageFont from .helper import ( assert_image_equal, - assert_image_similar, + assert_image_similar_tofile, hopper, skip_unless_feature, ) @@ -71,7 +71,7 @@ def helper_arc(bbox, start, end): draw.arc(bbox, start, end) # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_arc.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) def test_arc1(): @@ -110,20 +110,19 @@ def test_arc_no_loops(): draw.arc(BBOX1, start=start, end=end) # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) def test_arc_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width.png" # Act draw.arc(BBOX1, 10, 260, width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) def test_arc_width_pieslice_large(): @@ -131,26 +130,24 @@ def test_arc_width_pieslice_large(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_pieslice.png" # Act draw.arc(BBOX1, 10, 260, fill="yellow", width=100) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) def test_arc_width_fill(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_arc_width_fill.png" # Act draw.arc(BBOX1, 10, 260, fill="yellow", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) def test_arc_width_non_whole_angle(): @@ -163,7 +160,7 @@ def test_arc_width_non_whole_angle(): draw.arc(BBOX1, 10, 259.5, width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_bitmap(): @@ -190,7 +187,7 @@ def helper_chord(mode, bbox, start, end): draw.chord(bbox, start, end, fill="red", outline="yellow") # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_chord1(): @@ -209,26 +206,24 @@ def test_chord_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_width.png" # Act draw.chord(BBOX1, 10, 260, outline="yellow", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) def test_chord_width_fill(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_chord_width_fill.png" # Act draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) def test_chord_zero_width(): @@ -254,7 +249,7 @@ def helper_ellipse(mode, bbox): draw.ellipse(bbox, fill="green", outline="blue") # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_ellipse1(): @@ -276,8 +271,8 @@ def test_ellipse_translucent(): draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) # Assert - expected = Image.open("Tests/images/imagedraw_ellipse_translucent.png") - assert_image_similar(im, expected, 1) + expected = "Tests/images/imagedraw_ellipse_translucent.png" + assert_image_similar_tofile(im, expected, 1) def test_ellipse_edge(): @@ -289,7 +284,7 @@ def test_ellipse_edge(): draw.ellipse(((0, 0), (W - 1, H)), fill="white") # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) def test_ellipse_symmetric(): @@ -304,39 +299,36 @@ def test_ellipse_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width.png" # Act draw.ellipse(BBOX1, outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) def test_ellipse_width_large(): # Arrange im = Image.new("RGB", (500, 500)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width_large.png" # Act draw.ellipse((25, 25, 475, 475), outline="blue", width=75) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) def test_ellipse_width_fill(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_ellipse_width_fill.png" # Act draw.ellipse(BBOX1, fill="green", outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) def test_ellipse_zero_width(): @@ -423,7 +415,7 @@ def helper_pieslice(bbox, start, end): draw.pieslice(bbox, start, end, fill="white", outline="blue") # Assert - assert_image_similar(im, Image.open("Tests/images/imagedraw_pieslice.png"), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) def test_pieslice1(): @@ -440,13 +432,12 @@ def test_pieslice_width(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_pieslice_width.png" # Act draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) def test_pieslice_width_fill(): @@ -459,7 +450,7 @@ def test_pieslice_width_fill(): draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) def test_pieslice_zero_width(): @@ -571,13 +562,12 @@ def test_big_rectangle(): im = Image.new("RGB", (W, H)) bbox = [(-1, -1), (W + 1, H + 1)] draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_big_rectangle.png" # Act draw.rectangle(bbox, fill="orange") # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) def test_rectangle_width(): @@ -878,13 +868,12 @@ def test_wide_line_dot(): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_wide_line_dot.png" # Act draw.line([(50, 50), (50, 50)], width=3) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) def test_wide_line_larger_than_int(): @@ -897,7 +886,7 @@ def test_wide_line_larger_than_int(): draw.line([(0, 0), (32768, 32768)], width=3) # Assert - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) @pytest.mark.parametrize( @@ -984,13 +973,12 @@ def test_wide_line_larger_than_int(): def test_line_joint(xy): im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) - expected = "Tests/images/imagedraw_line_joint_curve.png" # Act draw.line(xy, GRAY, 50, "curve") # Assert - assert_image_similar(im, Image.open(expected), 3) + assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) def test_textsize_empty_string(): @@ -1031,8 +1019,8 @@ def test_stroke(): draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill) # Assert - assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 3.1 + assert_image_similar_tofile( + im, "Tests/images/imagedraw_stroke_" + suffix + ".png", 3.1 ) @@ -1047,9 +1035,7 @@ def test_stroke_descender(): draw.text((10, 0), "y", "#f00", font, stroke_width=2, stroke_fill="#0f0") # Assert - assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_descender.png"), 6.76 - ) + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) @skip_unless_feature("freetype2") @@ -1065,9 +1051,7 @@ def test_stroke_multiline(): ) # Assert - assert_image_similar( - im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3 - ) + assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) def test_same_color_outline(): @@ -1106,4 +1090,4 @@ def test_same_color_outline(): expected = "Tests/images/imagedraw_outline_{}_{}.png".format( operation, mode ) - assert_image_similar(im, Image.open(expected), 1) + assert_image_similar_tofile(im, expected, 1) From 824f2930269bb7001642248582692b9178fe3f22 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jun 2020 19:57:02 +1000 Subject: [PATCH 36/42] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index dad3e7d00..c79b9d1ef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.2.0 (unreleased) ------------------ +- Deprecate Image.show(command="...") #4646 + [nulano, hugovk, radarhere] + - Updated JPEG magic number #4707 [Cykooz, radarhere] From d7b812fcb2c942b36787837ca165ff2473defd32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 Jun 2020 21:50:23 +1000 Subject: [PATCH 37/42] Added release notes for #4646 [ci skip] --- docs/releasenotes/7.2.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 26a1464a4..6b91a1b01 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -37,6 +37,12 @@ are now read as just a single bytestring. Deprecations ^^^^^^^^^^^^ +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``command`` parameter was deprecated and will be removed in a future release. +Use a subclass of :py:class:`PIL.ImageShow.Viewer` instead. + ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ From ee06255ff0ee64f8d3d1062c75e455973cc9d2fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Jun 2020 22:17:18 +1000 Subject: [PATCH 38/42] Deprecated _showxv --- Tests/test_image.py | 18 +++++++++++++++++- docs/deprecations.rst | 9 +++++++++ docs/releasenotes/7.2.0.rst | 7 +++++++ src/PIL/Image.py | 9 +++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index f284f89a7..be67f5947 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -5,7 +5,7 @@ import tempfile import PIL import pytest -from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError +from PIL import Image, ImageDraw, ImagePalette, ImageShow, UnidentifiedImageError from .helper import ( assert_image_equal, @@ -585,6 +585,22 @@ class TestImage: expected = Image.new(mode, (100, 100), color) assert_image_equal(im.convert(mode), expected) + def test_showxv_deprecation(self): + class TestViewer(ImageShow.Viewer): + def show_image(self, image, **options): + return True + + viewer = TestViewer() + ImageShow.register(viewer, -1) + + im = Image.new("RGB", (50, 50), "white") + + with pytest.warns(DeprecationWarning): + Image._showxv(im) + + # Restore original state + ImageShow._viewers.pop(0) + def test_no_resource_warning_on_save(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/835 # Arrange diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 38d2143c4..f78842ac1 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -20,6 +20,15 @@ Image.show command parameter The ``command`` parameter was deprecated and will be removed in a future release. Use a subclass of ``ImageShow.Viewer`` instead. +Image._showxv +~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 + +``Image._showxv`` has been deprecated. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/7.2.0.rst b/docs/releasenotes/7.2.0.rst index 6b91a1b01..ff1b7c9e7 100644 --- a/docs/releasenotes/7.2.0.rst +++ b/docs/releasenotes/7.2.0.rst @@ -43,6 +43,13 @@ Image.show command parameter The ``command`` parameter was deprecated and will be removed in a future release. Use a subclass of :py:class:`PIL.ImageShow.Viewer` instead. +Image._showxv +~~~~~~~~~~~~~ + +``Image._showxv`` has been deprecated. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + ImageFile.raise_ioerror ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 02c73bc49..7a2ae02d6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3150,12 +3150,21 @@ def register_encoder(name, encoder): def _show(image, **options): + options["_internal_pillow"] = True _showxv(image, **options) def _showxv(image, title=None, **options): from . import ImageShow + if "_internal_pillow" in options: + del options["_internal_pillow"] + else: + warnings.warn( + "_showxv is deprecated and will be removed in a future release. " + "Use Image.show instead.", + DeprecationWarning, + ) ImageShow.show(image, title, **options) From 685c0439b8ef3c0f9f7be4837a751d22f07cbb87 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 Jun 2020 18:47:34 +1000 Subject: [PATCH 39/42] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c79b9d1ef..c5b55a524 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.2.0 (unreleased) ------------------ +- Deprecated _showxv #4714 + [radarhere] + - Deprecate Image.show(command="...") #4646 [nulano, hugovk, radarhere] From d641bdc504a943b21eaa770e3a549dd6b33d15af Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 Jun 2020 14:05:34 +0300 Subject: [PATCH 40/42] Fix isort --- Tests/test_imagegrab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index a1453a07e..ae1277ced 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -4,6 +4,7 @@ import sys import pytest from PIL import Image, ImageGrab + from .helper import assert_image, assert_image_equal_tofile, skip_unless_feature From 19dd5cbfab75f6d9e74349a611140a5aa2fcd85e Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 22 Jun 2020 09:00:17 +0200 Subject: [PATCH 41/42] fix some function references (cherry picked from commit 9fb582940d577857d9034e0bf0c5cf5630c2d42e) --- docs/handbook/image-file-formats.rst | 22 +++++++++++----------- src/PIL/ImageDraw2.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 40db9fe2b..6bcff7135 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -8,7 +8,7 @@ Over 30 different file formats can be identified and read by the library. Write support is less extensive, but most common interchange and presentation formats are supported. -The :py:meth:`~PIL.Image.Image.open` function identifies files from their +The :py:meth:`~PIL.Image.open` function identifies files from their contents, not their names, but the :py:meth:`~PIL.Image.Image.save` method looks at the name to determine which format to use, unless the format is given explicitly. @@ -25,7 +25,7 @@ Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P` or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding is not supported. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **compression** @@ -74,7 +74,7 @@ are used or GIF89a is already in use. Note that GIF files are always read as grayscale (``L``) or palette mode (``P``) images. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **background** @@ -203,7 +203,7 @@ ICNS Pillow reads and (macOS only) writes macOS ``.icns`` files. By default, the largest available icon is read, though you can override this by setting the :py:attr:`~PIL.Image.Image.size` property before calling -:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.Image.open` method +:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` property: **sizes** @@ -257,7 +257,7 @@ Using the :py:meth:`~PIL.Image.Image.draft` method, you can speed things up by converting ``RGB`` images to ``L``, and resize images to 1/2, 1/4 or 1/8 of their original size while loading them. -The :py:meth:`~PIL.Image.Image.open` method may set the following +The :py:meth:`~PIL.Image.open` method may set the following :py:attr:`~PIL.Image.Image.info` properties if available: **jfif** @@ -697,7 +697,7 @@ Pillow also reads SPIDER stack files containing sequences of SPIDER images. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and random access is allowed. -The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: +The :py:meth:`~PIL.Image.open` method sets the following attributes: **format** Set to ``SPIDER`` @@ -750,7 +750,7 @@ uncompressed files. support for reading Packbits, LZW and JPEG compressed TIFFs without using libtiff. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **compression** @@ -1021,7 +1021,7 @@ FLI, FLC Pillow reads Autodesk FLI and FLC animations. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **duration** @@ -1054,7 +1054,7 @@ GBR The GBR decoder reads GIMP brush files, version 1 and 2. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **comment** @@ -1069,7 +1069,7 @@ GD Pillow reads uncompressed GD2 files. Note that you must use :py:func:`PIL.GdImageFile.open` to read such a file. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **transparency** @@ -1185,7 +1185,7 @@ XPM Pillow reads X pixmap files (mode ``P``) with 256 colors or less. -The :py:meth:`~PIL.Image.Image.open` method sets the following +The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: **transparency** diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index b14b68e3e..1f63110fd 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -106,7 +106,7 @@ class Draw: def chord(self, xy, start, end, *options): """ - Same as :py:meth:`~PIL.ImageDraw2.ImageDraw.arc`, but connects the end points + Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points with a straight line. .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` From 4f1ee7a881f2ce08ae8f37a0d49241050f2d55b7 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 22 Jun 2020 10:55:54 +0200 Subject: [PATCH 42/42] add missing and sort Image functions (cherry picked from commit f31c786aa6dadfcd93596887bb67b1d9a776f8c6) --- docs/reference/Image.rst | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 216fa1196..649c9a185 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -76,9 +76,16 @@ Constructing images .. autofunction:: new .. autofunction:: fromarray .. autofunction:: frombytes -.. autofunction:: fromstring .. autofunction:: frombuffer +Generating images +^^^^^^^^^^^^^^^^^ + +.. autofunction:: effect_mandelbrot +.. autofunction:: effect_noise +.. autofunction:: linear_gradient +.. autofunction:: radial_gradient + Registering plugins ^^^^^^^^^^^^^^^^^^^ @@ -88,12 +95,14 @@ Registering plugins ignore them. .. autofunction:: register_open -.. autofunction:: register_decoder .. autofunction:: register_mime .. autofunction:: register_save -.. autofunction:: register_encoder +.. autofunction:: register_save_all .. autofunction:: register_extension - +.. autofunction:: register_extensions +.. autofunction:: registered_extensions +.. autofunction:: register_decoder +.. autofunction:: register_encoder The Image Class --------------- @@ -140,6 +149,8 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.draft +.. automethod:: PIL.Image.Image.effect_spread +.. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter This blurs the input image using a filter from the ``ImageFilter`` module: @@ -176,12 +187,14 @@ This helps to get the bounding box coordinates of the input image: print(im.getbbox()) # Returns four coordinates in the format (left, upper, right, lower) +.. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata -.. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getexif +.. automethod:: PIL.Image.Image.getextrema .. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpixel +.. automethod:: PIL.Image.Image.getprojection .. automethod:: PIL.Image.Image.histogram .. automethod:: PIL.Image.Image.offset .. automethod:: PIL.Image.Image.paste @@ -191,6 +204,8 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.putpalette .. automethod:: PIL.Image.Image.putpixel .. automethod:: PIL.Image.Image.quantize +.. automethod:: PIL.Image.Image.reduce +.. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: @@ -205,7 +220,6 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` (width, height) = (im.width // 2, im.height // 2) im_resized = im.resize((width, height)) -.. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.rotate This rotates the input image by ``theta`` degrees counter clockwise: @@ -225,7 +239,6 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.seek .. automethod:: PIL.Image.Image.show .. automethod:: PIL.Image.Image.split -.. automethod:: PIL.Image.Image.getchannel .. automethod:: PIL.Image.Image.tell .. automethod:: PIL.Image.Image.thumbnail .. automethod:: PIL.Image.Image.tobitmap