Merge branch 'master' into winbuild-rewrite

This commit is contained in:
Andrew Murray 2020-05-02 00:20:14 +10:00 committed by GitHub
commit c04013fa74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2319 additions and 2311 deletions

View File

@ -14,6 +14,7 @@ jobs:
arch,
ubuntu-16.04-xenial-amd64,
ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
debian-9-stretch-x86,
debian-10-buster-x86,
centos-6-amd64,
@ -21,8 +22,8 @@ jobs:
centos-8-amd64,
amazon-1-amd64,
amazon-2-amd64,
fedora-30-amd64,
fedora-31-amd64,
fedora-32-amd64,
]
dockerTag: [master]

View File

@ -28,6 +28,9 @@ matrix:
- python: "pypy3"
name: "PyPy3 Xenial"
- python: "3.9-dev"
name: "3.9-dev Xenial"
services: xvfb
- python: "3.8"
name: "3.8 Xenial"
services: xvfb

View File

@ -2,6 +2,21 @@
Changelog (Pillow)
==================
7.2.0 (unreleased)
------------------
- Fixed bug when unpickling TIFF images #4565
[radarhere]
- Fix pickling WebP #4561
[hugovk, radarhere]
7.1.2 (2020-04-25)
------------------
- Raise an EOFError when seeking too far in PNG #4528
[radarhere]
7.1.1 (2020-04-02)
------------------

View File

@ -21,10 +21,8 @@ exclude .appveyor.yml
exclude .coveragerc
exclude .editorconfig
exclude .readthedocs.yml
exclude azure-pipelines.yml
exclude codecov.yml
global-exclude .git*
global-exclude *.pyc
global-exclude *.so
prune .azure-pipelines
prune .ci

View File

@ -272,12 +272,8 @@ def on_github_actions():
def on_ci():
# Travis and AppVeyor have "CI"
# Azure Pipelines has "TF_BUILD"
# GitHub Actions has "GITHUB_ACTIONS"
return (
"CI" in os.environ or "TF_BUILD" in os.environ or "GITHUB_ACTIONS" in os.environ
)
# GitHub Actions, Travis and AppVeyor have "CI"
return "CI" in os.environ
def is_big_endian():

View File

@ -66,7 +66,7 @@ def test_load_set_dpi():
assert im.size == (164, 164)
with Image.open("Tests/images/drawing_wmf_ref_144.png") as expected:
assert_image_similar(im, expected, 2.0)
assert_image_similar(im, expected, 2.1)
def test_save(tmp_path):

View File

@ -95,8 +95,9 @@ class TestImageFile:
def test_raise_ioerror(self):
with pytest.raises(IOError):
with pytest.raises(DeprecationWarning):
with pytest.warns(DeprecationWarning) as record:
ImageFile.raise_ioerror(1)
assert len(record) == 1
def test_raise_oserror(self):
with pytest.raises(OSError):

View File

@ -1,11 +1,14 @@
import pickle
import pytest
from PIL import Image
from .helper import skip_unless_feature
def helper_pickle_file(tmp_path, pickle, protocol=0, mode=None):
def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode):
# Arrange
with Image.open("Tests/images/hopper.jpg") as im:
with Image.open(test_file) as im:
filename = str(tmp_path / "temp.pkl")
if mode:
im = im.convert(mode)
@ -20,9 +23,7 @@ def helper_pickle_file(tmp_path, pickle, protocol=0, mode=None):
assert im == loaded_im
def helper_pickle_string(
pickle, protocol=0, test_file="Tests/images/hopper.jpg", mode=None
):
def helper_pickle_string(pickle, protocol, test_file, mode):
with Image.open(test_file) as im:
if mode:
im = im.convert(mode)
@ -35,41 +36,31 @@ def helper_pickle_string(
assert im == loaded_im
def test_pickle_image(tmp_path):
@pytest.mark.parametrize(
("test_file", "test_mode"),
[
("Tests/images/hopper.jpg", None),
("Tests/images/hopper.jpg", "L"),
("Tests/images/hopper.jpg", "PA"),
pytest.param(
"Tests/images/hopper.webp", None, marks=skip_unless_feature("webp")
),
("Tests/images/hopper.tif", None),
("Tests/images/test-card.png", None),
("Tests/images/zero_bb.png", None),
("Tests/images/zero_bb_scale2.png", None),
("Tests/images/non_zero_bb.png", None),
("Tests/images/non_zero_bb_scale2.png", None),
("Tests/images/p_trns_single.png", None),
("Tests/images/pil123p.png", None),
("Tests/images/itxt_chunks.png", None),
],
)
def test_pickle_image(tmp_path, test_file, test_mode):
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
helper_pickle_string(pickle, protocol)
helper_pickle_file(tmp_path, pickle, protocol)
def test_pickle_p_mode():
# Act / Assert
for test_file in [
"Tests/images/test-card.png",
"Tests/images/zero_bb.png",
"Tests/images/zero_bb_scale2.png",
"Tests/images/non_zero_bb.png",
"Tests/images/non_zero_bb_scale2.png",
"Tests/images/p_trns_single.png",
"Tests/images/pil123p.png",
"Tests/images/itxt_chunks.png",
]:
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
helper_pickle_string(pickle, protocol=protocol, test_file=test_file)
def test_pickle_pa_mode(tmp_path):
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
helper_pickle_string(pickle, protocol, mode="PA")
helper_pickle_file(tmp_path, pickle, protocol, mode="PA")
def test_pickle_l_mode(tmp_path):
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
helper_pickle_string(pickle, protocol, mode="L")
helper_pickle_file(tmp_path, pickle, protocol, mode="L")
helper_pickle_string(pickle, protocol, test_file, test_mode)
helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode)
def test_pickle_la_mode_with_palette(tmp_path):
@ -88,3 +79,15 @@ def test_pickle_la_mode_with_palette(tmp_path):
im.mode = "PA"
assert im == loaded_im
@skip_unless_feature("webp")
def test_pickle_tell():
# Arrange
image = Image.open("Tests/images/hopper.webp")
# Act: roundtrip
unpickled_image = pickle.loads(pickle.dumps(image))
# Assert
assert unpickled_image.tell() == 0

View File

@ -394,14 +394,18 @@ These platforms are built and tested for every change.
+----------------------------------+--------------------------+-----------------------+
| Debian 10 Buster | 3.7 |x86 |
+----------------------------------+--------------------------+-----------------------+
| Fedora 30 | 3.7 |x86-64 |
+----------------------------------+--------------------------+-----------------------+
| Fedora 31 | 3.7 |x86-64 |
+----------------------------------+--------------------------+-----------------------+
| Fedora 32 | 3.8 |x86-64 |
+----------------------------------+--------------------------+-----------------------+
| macOS 10.15 Catalina | 3.5, 3.6, 3.7, 3.8, PyPy3|x86-64 |
+----------------------------------+--------------------------+-----------------------+
| Ubuntu Linux 16.04 LTS | 3.5, 3.6, 3.7, 3.8, PyPy3|x86-64 |
+----------------------------------+--------------------------+-----------------------+
| Ubuntu Linux 18.04 LTS | 3.6 |x86-64 |
+----------------------------------+--------------------------+-----------------------+
| Ubuntu Linux 20.04 LTS | 3.8 |x86-64 |
+----------------------------------+--------------------------+-----------------------+
| Windows Server 2016 | 3.8 |x86 |
| +--------------------------+-----------------------+
| | 3.5 |x86-64 |

View File

@ -100,6 +100,25 @@ Example: Draw Partial Opacity Text
out.show()
Example: Draw Multiline Text
----------------------------
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
# create an image
out = Image.new("RGB", (150, 100), (255, 255, 255))
# get a font
fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40)
# get a drawing context
d = ImageDraw.Draw(out)
# draw multiline text
d.multiline_text((10,10), "Hello\nWorld", font=fnt, fill=(0, 0, 0))
out.show()
Functions

View File

@ -0,0 +1,16 @@
7.1.2
-----
Fix another regression seeking PNG files
========================================
This fixes a regression introduced in 7.1.0 when adding support for APNG files.
When calling ``seek(n)`` on a regular PNG where ``n > 0``, it failed to raise an
``EOFError`` as it should have done, resulting in:
.. code-block:: python
AttributeError: 'NoneType' object has no attribute 'read'
Pillow 7.1.2 now raises the correct exception.

View File

@ -6,6 +6,7 @@ Release Notes
.. toctree::
:maxdepth: 2
7.1.2
7.1.1
7.1.0
7.0.0

View File

@ -59,16 +59,10 @@ class DcxImageFile(PcxImageFile):
self.__fp = self.fp
self.frame = None
self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1
self.seek(0)
@property
def n_frames(self):
return len(self._offset)
@property
def is_animated(self):
return len(self._offset) > 1
def seek(self, frame):
if not self._seek_check(frame):
return

View File

@ -51,7 +51,8 @@ class FliImageFile(ImageFile.ImageFile):
raise SyntaxError("not an FLI/FLC file")
# frames
self.__framecount = i16(s[6:8])
self.n_frames = i16(s[6:8])
self.is_animated = self.n_frames > 1
# image characteristics
self.mode = "P"
@ -110,14 +111,6 @@ class FliImageFile(ImageFile.ImageFile):
palette[i] = (r, g, b)
i += 1
@property
def n_frames(self):
return self.__framecount
@property
def is_animated(self):
return self.__framecount > 1
def seek(self, frame):
if not self._seek_check(frame):
return

View File

@ -2247,6 +2247,7 @@ class Image:
:py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.LANCZOS`.
If omitted, it defaults to :py:attr:`PIL.Image.BICUBIC`.
(was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0).
See: :ref:`concept-filters`.
:param reducing_gap: Apply optimization by resizing the image
in two steps. First, reducing the image by integer times
using :py:meth:`~PIL.Image.Image.reduce` or
@ -2324,7 +2325,7 @@ class Image:
It may also be an object with a :py:meth:`~method.getdata` method
that returns a tuple supplying new **method** and **data** values::
class Example(object):
class Example:
def getdata(self):
method = Image.EXTENT
data = (0, 0, 100, 100)
@ -2336,6 +2337,7 @@ class Image:
environment), or :py:attr:`PIL.Image.BICUBIC` (cubic spline
interpolation in a 4x4 environment). If omitted, or if the image
has mode "1" or "P", it is set to :py:attr:`PIL.Image.NEAREST`.
See: :ref:`concept-filters`.
:param fill: If **method** is an
:py:class:`~PIL.Image.ImageTransformHandler` object, this is one of
the arguments passed to it. Otherwise, it is unused.

View File

@ -150,10 +150,10 @@ class ImageFile(Image.Image):
def load(self):
"""Load image data based on tile list"""
pixel = Image.Image.load(self)
if self.tile is None:
raise OSError("cannot load this image")
pixel = Image.Image.load(self)
if not self.tile:
return pixel

View File

@ -64,20 +64,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.__fp = self.fp
self.frame = None
self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1
if len(self.images) > 1:
self.category = Image.CONTAINER
self.seek(0)
@property
def n_frames(self):
return len(self.images)
@property
def is_animated(self):
return len(self.images) > 1
def seek(self, frame):
if not self._seek_check(frame):
return

View File

@ -48,15 +48,16 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
def _after_jpeg_open(self, mpheader=None):
self.mpinfo = mpheader if mpheader is not None else self._getmp()
self.__framecount = self.mpinfo[0xB001]
self.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [
mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
]
self.__mpoffsets[0] = 0
# Note that the following assertion will only be invalid if something
# gets broken within JpegImagePlugin.
assert self.__framecount == len(self.__mpoffsets)
assert self.n_frames == len(self.__mpoffsets)
del self.info["mpoffset"] # no longer needed
self.is_animated = self.n_frames > 1
self.__fp = self.fp # FIXME: hack
self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame
self.__frame = 0
@ -67,14 +68,6 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
def load_seek(self, pos):
self.__fp.seek(pos)
@property
def n_frames(self):
return self.__framecount
@property
def is_animated(self):
return self.__framecount > 1
def seek(self, frame):
if not self._seek_check(frame):
return

View File

@ -119,6 +119,8 @@ class PsdImageFile(ImageFile.ImageFile):
if size:
self.layers = _layerinfo(self.fp)
self.fp.seek(end)
self.n_frames = len(self.layers)
self.is_animated = self.n_frames > 1
#
# image descriptor
@ -130,14 +132,6 @@ class PsdImageFile(ImageFile.ImageFile):
self.frame = 1
self._min_frame = 1
@property
def n_frames(self):
return len(self.layers)
@property
def is_animated(self):
return len(self.layers) > 1
def seek(self, layer):
if not self._seek_check(layer):
return

View File

@ -1015,10 +1015,6 @@ class TiffImageFile(ImageFile.ImageFile):
self.seek(current)
return self._n_frames
@property
def is_animated(self):
return self._is_animated
def seek(self, frame):
"""Select a given frame as current image"""
if not self._seek_check(frame):
@ -1052,7 +1048,7 @@ class TiffImageFile(ImageFile.ImageFile):
if self.__next == 0:
self._n_frames = frame + 1
if len(self._frame_pos) == 1:
self._is_animated = self.__next != 0
self.is_animated = self.__next != 0
self.__frame += 1
self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp)
@ -1066,7 +1062,7 @@ class TiffImageFile(ImageFile.ImageFile):
return self.__frame
def load(self):
if self.use_load_libtiff:
if self.tile and self.use_load_libtiff:
return self._load_libtiff()
return super().load()
@ -1087,19 +1083,14 @@ class TiffImageFile(ImageFile.ImageFile):
# allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below.
if not self._is_animated:
if not self.is_animated:
self._close_exclusive_fp_after_loading = True
def _load_libtiff(self):
""" Overload method triggered when we detect a compressed tiff
Calls out to libtiff """
pixel = Image.Image.load(self)
if self.tile is None:
raise OSError("cannot load this image")
if not self.tile:
return pixel
Image.Image.load(self)
self.load_prepare()
@ -1138,7 +1129,7 @@ class TiffImageFile(ImageFile.ImageFile):
except ValueError:
raise OSError("Couldn't set the image")
close_self_fp = self._exclusive_fp and not self._is_animated
close_self_fp = self._exclusive_fp and not self.is_animated
if hasattr(self.fp, "getvalue"):
# We've got a stringio like thing passed in. Yay for all in memory.
# The decoder needs the entire file in one shot, so there's not

View File

@ -38,6 +38,8 @@ class WebPImageFile(ImageFile.ImageFile):
format = "WEBP"
format_description = "WebP image"
__loaded = 0
__logical_frame = 0
def _open(self):
if not _webp.HAVE_WEBPANIM:
@ -52,7 +54,8 @@ class WebPImageFile(ImageFile.ImageFile):
self._size = width, height
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
self._n_frames = 1
self.n_frames = 1
self.is_animated = False
return
# Use the newer AnimDecoder API to parse the (possibly) animated file,
@ -70,7 +73,8 @@ class WebPImageFile(ImageFile.ImageFile):
bgcolor & 0xFF,
)
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
self._n_frames = frame_count
self.n_frames = frame_count
self.is_animated = self.n_frames > 1
self.mode = "RGB" if mode == "RGBX" else mode
self.rawmode = mode
self.tile = []
@ -88,30 +92,15 @@ class WebPImageFile(ImageFile.ImageFile):
# Initialize seek state
self._reset(reset=False)
self.seek(0)
def _getexif(self):
if "exif" not in self.info:
return None
return dict(self.getexif())
@property
def n_frames(self):
return self._n_frames
@property
def is_animated(self):
return self._n_frames > 1
def seek(self, frame):
if not _webp.HAVE_WEBPANIM:
return super().seek(frame)
# Perform some simple checks first
if frame >= self._n_frames:
raise EOFError("attempted to seek beyond end of sequence")
if frame < 0:
raise EOFError("negative frame index is not valid")
if not self._seek_check(frame):
return
# Set logical frame to requested position
self.__logical_frame = frame

View File

@ -823,6 +823,7 @@ static int decode_bcn(Imaging im, ImagingCodecState state, const UINT8* src, int
if (state->y >= ymax) return -1; \
} \
break
DECODE_LOOP(1, 8, rgba);
DECODE_LOOP(2, 16, rgba);
DECODE_LOOP(3, 16, rgba);