diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index b3cfb99bb..2b4dc6b52 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -11,9 +11,9 @@ jobs:
matrix:
docker: [
# Run slower jobs first to give them a headstart and reduce waiting time
- ubuntu-20.04-focal-arm64v8,
- ubuntu-20.04-focal-ppc64le,
- ubuntu-20.04-focal-s390x,
+ ubuntu-22.04-jammy-arm64v8,
+ ubuntu-22.04-jammy-ppc64le,
+ ubuntu-22.04-jammy-s390x,
# Then run the remainder
alpine,
amazon-2-amd64,
@@ -32,11 +32,11 @@ jobs:
]
dockerTag: [main]
include:
- - docker: "ubuntu-20.04-focal-arm64v8"
+ - docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64"
- - docker: "ubuntu-20.04-focal-ppc64le"
+ - docker: "ubuntu-22.04-jammy-ppc64le"
qemu-arch: "ppc64le"
- - docker: "ubuntu-20.04-focal-s390x"
+ - docker: "ubuntu-22.04-jammy-s390x"
qemu-arch: "s390x"
name: ${{ matrix.docker }}
diff --git a/CHANGES.rst b/CHANGES.rst
index c5bf6b5f8..4bcd9d5ff 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,24 @@ Changelog (Pillow)
9.2.0 (unreleased)
------------------
+- Separate multiple GIF comment blocks with newlines #6294
+ [raygard, radarhere]
+
+- Always use GIF89a for comments #6292
+ [raygard, radarhere]
+
+- Ignore compression value from BMP info dictionary when saving as TIFF #6231
+ [radarhere]
+
+- If font is file-like object, do not re-read from object to get variant #6234
+ [radarhere]
+
+- Raise ValueError when trying to access internal fp after close #6213
+ [radarhere]
+
+- Support more affine expression forms in im.point() #6254
+ [benrg, radarhere]
+
- Populate Python palette in fromarray() #6283
[radarhere]
@@ -17,9 +35,6 @@ Changelog (Pillow)
- Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270
[radarhere]
-- Do not open images with zero or negative height #6269
- [radarhere]
-
- Search pkgconf system libs/cflags #6138
[jameshilliard, radarhere]
@@ -50,6 +65,15 @@ Changelog (Pillow)
- Deprecated PhotoImage.paste() box parameter #6178
[radarhere]
+9.1.1 (2022-05-17)
+------------------
+
+- When reading past the end of a TGA scan line, reduce bytes left. CVE-2022-30595
+ [radarhere]
+
+- Do not open images with zero or negative height #6269
+ [radarhere]
+
9.1.0 (2022-04-01)
------------------
diff --git a/Makefile b/Makefile
index 437050ed4..219dda1de 100644
--- a/Makefile
+++ b/Makefile
@@ -85,6 +85,8 @@ release-test:
sdist:
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
python3 -m build --sdist
+ python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
+ python3 -m twine check --strict dist/*
.PHONY: test
test:
diff --git a/RELEASING.md b/RELEASING.md
index a6049b685..aa7511c8a 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -24,7 +24,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create and check source distribution:
```bash
make sdist
- python3 -m twine check --strict dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
@@ -61,7 +60,6 @@ Released as needed for security, installation or critical bug fixes.
* [ ] Create and check source distribution:
```bash
make sdist
- python3 -m twine check --strict dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
@@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related
* [ ] Create and check source distribution:
```bash
make sdist
- python3 -m twine check --strict dist/*
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
diff --git a/Tests/images/cross_scan_line_truncated.tga b/Tests/images/cross_scan_line_truncated.tga
new file mode 100644
index 000000000..cec4357e3
Binary files /dev/null and b/Tests/images/cross_scan_line_truncated.tga differ
diff --git a/Tests/images/multiple_comments.gif b/Tests/images/multiple_comments.gif
new file mode 100644
index 000000000..88b2af800
Binary files /dev/null and b/Tests/images/multiple_comments.gif differ
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index d1d5c85c1..ad61a07cc 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -637,6 +637,15 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
+def test_seek_after_close():
+ im = Image.open("Tests/images/apng/delay.png")
+ im.seek(1)
+ im.close()
+
+ with pytest.raises(ValueError):
+ im.seek(0)
+
+
def test_constants_deprecation():
for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_",
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index c1ad4a7f0..a7d43d2e9 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -46,6 +46,15 @@ def test_closed_file():
im.close()
+def test_seek_after_close():
+ im = Image.open(animated_test_file)
+ im.seek(1)
+ im.close()
+
+ with pytest.raises(ValueError):
+ im.seek(0)
+
+
def test_context_manager():
with warnings.catch_warnings():
with Image.open(static_test_file) as im:
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 762dab8df..3f7b7aeb9 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -46,6 +46,19 @@ def test_closed_file():
im.close()
+def test_seek_after_close():
+ im = Image.open("Tests/images/iss634.gif")
+ im.load()
+ im.close()
+
+ with pytest.raises(ValueError):
+ im.is_animated
+ with pytest.raises(ValueError):
+ im.n_frames
+ with pytest.raises(ValueError):
+ im.seek(1)
+
+
def test_context_manager():
with warnings.catch_warnings():
with Image.open(TEST_GIF) as im:
@@ -794,6 +807,9 @@ def test_comment(tmp_path):
with Image.open(out) as reread:
assert reread.info["comment"] == im.info["comment"].encode()
+ # Test that GIF89a is used for comments
+ assert reread.info["version"] == b"GIF89a"
+
def test_comment_over_255(tmp_path):
out = str(tmp_path / "temp.gif")
@@ -804,15 +820,23 @@ def test_comment_over_255(tmp_path):
im.info["comment"] = comment
im.save(out)
with Image.open(out) as reread:
-
assert reread.info["comment"] == comment
+ # Test that GIF89a is used for comments
+ assert reread.info["version"] == b"GIF89a"
+
def test_zero_comment_subblocks():
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
assert_image_equal_tofile(im, TEST_GIF)
+def test_read_multiple_comment_blocks():
+ with Image.open("Tests/images/multiple_comments.gif") as im:
+ # Multiple comment blocks in a frame are separated not concatenated
+ assert im.info["comment"] == b"Test comment 1\nTest comment 2"
+
+
def test_write_comment(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/dispose_prev.gif") as im:
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index d83c584b5..588b9b703 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -18,6 +18,7 @@ from .helper import (
hopper,
mark_if_feature_version,
skip_unless_feature,
+ skip_unless_feature_version,
)
@@ -991,6 +992,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im:
im.load()
+ @skip_unless_feature_version("libtiff", "4.0.4")
def test_realloc_overflow(self):
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index ca3ea8419..d9b59321b 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -48,6 +48,14 @@ def test_closed_file():
im.close()
+def test_seek_after_close():
+ im = Image.open(test_files[0])
+ im.close()
+
+ with pytest.raises(ValueError):
+ im.seek(1)
+
+
def test_context_manager():
with warnings.catch_warnings():
with Image.open(test_files[0]) as im:
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index aeea3fb42..0c8c9f304 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -101,6 +101,10 @@ def test_cross_scan_line():
with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
+ with Image.open("Tests/images/cross_scan_line_truncated.tga") as im:
+ with pytest.raises(OSError):
+ im.load()
+
def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga"
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index 87e0c2d25..d03f7c736 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -70,6 +70,15 @@ class TestFileTiff:
im.load()
im.close()
+ def test_seek_after_close(self):
+ im = Image.open("Tests/images/multipage.tiff")
+ im.close()
+
+ with pytest.raises(ValueError):
+ im.n_frames
+ with pytest.raises(ValueError):
+ im.seek(1)
+
def test_context_manager(self):
with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im:
@@ -706,6 +715,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert reloaded.info["icc_profile"] == icc_profile
+ def test_save_bmp_compression(self, tmp_path):
+ with Image.open("Tests/images/hopper.bmp") as im:
+ assert im.info["compression"] == 0
+
+ outfile = str(tmp_path / "temp.tif")
+ im.save(outfile)
+
def test_discard_icc_profile(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py
index 428ad116b..157ecb120 100644
--- a/Tests/test_image_point.py
+++ b/Tests/test_image_point.py
@@ -1,5 +1,7 @@
import pytest
+from PIL import Image
+
from .helper import assert_image_equal, hopper
@@ -17,11 +19,24 @@ def test_sanity():
im.point(list(range(256)))
im.point(lambda x: x * 1)
im.point(lambda x: x + 1)
+ im.point(lambda x: x - 1)
im.point(lambda x: x * 1 + 1)
+ im.point(lambda x: 0.1 + 0.2 * x)
+ im.point(lambda x: -x)
+ im.point(lambda x: x - 0.5)
+ im.point(lambda x: 1 - x / 2)
+ im.point(lambda x: (2 + x) / 3)
+ im.point(lambda x: 0.5)
+ im.point(lambda x: x / 1)
+ im.point(lambda x: x + x)
with pytest.raises(TypeError):
- im.point(lambda x: x - 1)
+ im.point(lambda x: x * x)
with pytest.raises(TypeError):
- im.point(lambda x: x / 1)
+ im.point(lambda x: x / x)
+ with pytest.raises(TypeError):
+ im.point(lambda x: 1 / x)
+ with pytest.raises(TypeError):
+ im.point(lambda x: x // 2)
def test_16bit_lut():
@@ -47,3 +62,8 @@ def test_f_mode():
im = hopper("F")
with pytest.raises(ValueError):
im.point(None)
+
+
+def test_coerce_e_deprecation():
+ with pytest.warns(DeprecationWarning):
+ assert Image.coerce_e(2).data == 2
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 0e1d1e637..0c50303f9 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -65,9 +65,12 @@ class TestImageFont:
return font_bytes
def test_font_with_filelike(self):
- ImageFont.truetype(
+ ttf = ImageFont.truetype(
self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
)
+ ttf_copy = ttf.font_variant()
+ assert ttf_copy.font_bytes == ttf.font_bytes
+
self._render(self._font_as_bytes())
# Usage note: making two fonts from the same buffer fails.
# shared_bytes = self._font_as_bytes()
diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh
index 914e71e53..4f4b81a62 100755
--- a/depends/install_openjpeg.sh
+++ b/depends/install_openjpeg.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# install openjpeg
-archive=openjpeg-2.4.0
+archive=openjpeg-2.5.0
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index ad030acd0..8c5b8a748 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -170,6 +170,14 @@ in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 `_ or
`PySide6 `_ instead.
+Image.coerce_e
+~~~~~~~~~~~~~~
+
+.. deprecated:: 9.2.0
+
+This undocumented method has been deprecated and will be removed in Pillow 10
+(2023-07-01).
+
Removed features
----------------
diff --git a/docs/installation.rst b/docs/installation.rst
index 1807ecf9f..efde6e931 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -181,7 +181,8 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality.
- * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1** and **2.4.0**.
+ * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
+ **2.4.0** and **2.5.0**.
* Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie.
@@ -474,11 +475,9 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | PyPy3 | |
-| +----------------------------+---------------------+
-| | 3.8 | arm64v8, ppc64le, |
-| | | s390x |
+----------------------------------+----------------------------+---------------------+
-| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, |
+| | | s390x, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.7 | x86-64 |
+----------------------------------+----------------------------+---------------------+
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index 2ff9b3799..fe2658047 100644
--- a/docs/releasenotes/8.0.0.rst
+++ b/docs/releasenotes/8.0.0.rst
@@ -174,7 +174,7 @@ Previously, if a BMP file was too large, an ``OSError`` would be raised. Now,
Dark theme for docs
^^^^^^^^^^^^^^^^^^^
-The https://pillow.readthedocs.io documentation will use a dark theme if the the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
+The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst
new file mode 100644
index 000000000..f8b155f3d
--- /dev/null
+++ b/docs/releasenotes/9.1.1.rst
@@ -0,0 +1,16 @@
+9.1.1
+-----
+
+Security
+========
+
+This release addresses several security problems.
+
+:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines,
+Pillow reads the information past the end of the first line without deducting that
+from the length of the remaining file data. This vulnerability was introduced in Pillow
+9.1.0, and can cause a heap buffer overflow.
+
+Opening an image with a zero or negative height has been found to bypass a
+decompression bomb check. This will now raise a :py:exc:`SyntaxError` instead, in turn
+raising a ``PIL.UnidentifiedImageError``.
diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst
index c38944b10..db051d188 100644
--- a/docs/releasenotes/9.2.0.rst
+++ b/docs/releasenotes/9.2.0.rst
@@ -31,6 +31,14 @@ FreeTypeFont.getmask2 fill parameter
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2`
has been deprecated and will be removed in Pillow 10 (2023-07-01).
+Image.coerce_e
+~~~~~~~~~~~~~~
+
+.. deprecated:: 9.2.0
+
+This undocumented method has been deprecated and will be removed in Pillow 10
+(2023-07-01).
+
API Changes
===========
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index db578bdb7..597c804f8 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -15,6 +15,7 @@ expected to be backported to earlier versions.
:maxdepth: 2
9.2.0
+ 9.1.1
9.1.0
9.0.1
9.0.0
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index de21db8f0..aeed1e7c7 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -57,7 +57,7 @@ class DcxImageFile(PcxImageFile):
break
self._offset.append(offset)
- self.__fp = self.fp
+ self._fp = self.fp
self.frame = None
self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1
@@ -67,22 +67,13 @@ class DcxImageFile(PcxImageFile):
if not self._seek_check(frame):
return
self.frame = frame
- self.fp = self.__fp
+ self.fp = self._fp
self.fp.seek(self._offset[frame])
PcxImageFile._open(self)
def tell(self):
return self.frame
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index ea9503305..e13b1779c 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -91,7 +91,7 @@ class FliImageFile(ImageFile.ImageFile):
# set things up to decode first frame
self.__frame = -1
- self.__fp = self.fp
+ self._fp = self.fp
self.__rewind = self.fp.tell()
self.seek(0)
@@ -125,7 +125,7 @@ class FliImageFile(ImageFile.ImageFile):
def _seek(self, frame):
if frame == 0:
self.__frame = -1
- self.__fp.seek(self.__rewind)
+ self._fp.seek(self.__rewind)
self.__offset = 128
else:
# ensure that the previous frame was loaded
@@ -136,7 +136,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__frame = frame
# move to next frame
- self.fp = self.__fp
+ self.fp = self._fp
self.fp.seek(self.__offset)
s = self.fp.read(4)
@@ -153,15 +153,6 @@ class FliImageFile(ImageFile.ImageFile):
def tell(self):
return self.__frame
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
#
# registry
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 4d785d834..f5ec610cb 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -102,7 +102,7 @@ class GifImageFile(ImageFile.ImageFile):
p = ImagePalette.raw("RGB", p)
self.global_palette = self.palette = p
- self.__fp = self.fp # FIXME: hack
+ self._fp = self.fp # FIXME: hack
self.__rewind = self.fp.tell()
self._n_frames = None
self._is_animated = None
@@ -161,7 +161,7 @@ class GifImageFile(ImageFile.ImageFile):
self.__offset = 0
self.dispose = None
self.__frame = -1
- self.__fp.seek(self.__rewind)
+ self._fp.seek(self.__rewind)
self.disposal_method = 0
else:
# ensure that the previous frame was loaded
@@ -171,7 +171,7 @@ class GifImageFile(ImageFile.ImageFile):
if frame != self.__frame + 1:
raise ValueError(f"cannot seek to frame {frame}")
- self.fp = self.__fp
+ self.fp = self._fp
if self.__offset:
# backup to last frame
self.fp.seek(self.__offset)
@@ -228,12 +228,18 @@ class GifImageFile(ImageFile.ImageFile):
#
# comment extension
#
+ comment = b""
+
+ # Collect one comment block
while block:
- if "comment" in info:
- info["comment"] += block
- else:
- info["comment"] = block
+ comment += block
block = self.data()
+
+ if "comment" in info:
+ # If multiple comment blocks in frame, separate with \n
+ info["comment"] += b"\n" + comment
+ else:
+ info["comment"] = comment
s = None
continue
elif s[0] == 255:
@@ -281,7 +287,7 @@ class GifImageFile(ImageFile.ImageFile):
s = None
if interlace is None:
- # self.__fp = None
+ # self._fp = None
raise EOFError
if not update_image:
return
@@ -443,15 +449,6 @@ class GifImageFile(ImageFile.ImageFile):
def tell(self):
return self.__frame
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
# --------------------------------------------------------------------
# Write GIF files
@@ -903,17 +900,16 @@ def _get_global_header(im, info):
# https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
version = b"87a"
- for extensionKey in ["transparency", "duration", "loop", "comment"]:
- if info and extensionKey in info:
- if (extensionKey == "duration" and info[extensionKey] == 0) or (
- extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255)
- ):
- continue
- version = b"89a"
- break
- else:
- if im.info.get("version") == b"89a":
- version = b"89a"
+ if im.info.get("version") == b"89a" or (
+ info
+ and (
+ "transparency" in info
+ or "loop" in info
+ or info.get("duration")
+ or info.get("comment")
+ )
+ ):
+ version = b"89a"
background = _get_background(im, info.get("background"))
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 5563da4f5..78ccfb9cf 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -245,7 +245,7 @@ class ImImageFile(ImageFile.ImageFile):
self.__offset = offs = self.fp.tell()
- self.__fp = self.fp # FIXME: hack
+ self._fp = self.fp # FIXME: hack
if self.rawmode[:2] == "F;":
@@ -294,22 +294,13 @@ class ImImageFile(ImageFile.ImageFile):
size = ((self.size[0] * bits + 7) // 8) * self.size[1]
offs = self.__offset + frame * size
- self.fp = self.__fp
+ self.fp = self._fp
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
def tell(self):
return self.frame
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
#
# --------------------------------------------------------------------
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index c141da09f..5b7a50e18 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -29,7 +29,6 @@ import builtins
import io
import logging
import math
-import numbers
import os
import re
import struct
@@ -432,44 +431,50 @@ def _getencoder(mode, encoder_name, args, extra=()):
def coerce_e(value):
- return value if isinstance(value, _E) else _E(value)
+ deprecate("coerce_e", 10)
+ return value if isinstance(value, _E) else _E(1, value)
+# _E(scale, offset) represents the affine transformation scale * x + offset.
+# The "data" field is named for compatibility with the old implementation,
+# and should be renamed once coerce_e is removed.
class _E:
- def __init__(self, data):
+ def __init__(self, scale, data):
+ self.scale = scale
self.data = data
+ def __neg__(self):
+ return _E(-self.scale, -self.data)
+
def __add__(self, other):
- return _E((self.data, "__add__", coerce_e(other).data))
+ if isinstance(other, _E):
+ return _E(self.scale + other.scale, self.data + other.data)
+ return _E(self.scale, self.data + other)
+
+ __radd__ = __add__
+
+ def __sub__(self, other):
+ return self + -other
+
+ def __rsub__(self, other):
+ return other + -self
def __mul__(self, other):
- return _E((self.data, "__mul__", coerce_e(other).data))
+ if isinstance(other, _E):
+ return NotImplemented
+ return _E(self.scale * other, self.data * other)
+
+ __rmul__ = __mul__
+
+ def __truediv__(self, other):
+ if isinstance(other, _E):
+ return NotImplemented
+ return _E(self.scale / other, self.data / other)
def _getscaleoffset(expr):
- stub = ["stub"]
- data = expr(_E(stub)).data
- try:
- (a, b, c) = data # simplified syntax
- if a is stub and b == "__mul__" and isinstance(c, numbers.Number):
- return c, 0.0
- if a is stub and b == "__add__" and isinstance(c, numbers.Number):
- return 1.0, c
- except TypeError:
- pass
- try:
- ((a, b, c), d, e) = data # full syntax
- if (
- a is stub
- and b == "__mul__"
- and isinstance(c, numbers.Number)
- and d == "__add__"
- and isinstance(e, numbers.Number)
- ):
- return c, e
- except TypeError:
- pass
- raise ValueError("illegal expression")
+ a = expr(_E(1, 0))
+ return (a.scale, a.data) if isinstance(a, _E) else (0, a)
# --------------------------------------------------------------------
@@ -544,8 +549,10 @@ class Image:
def __exit__(self, *args):
if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
- if hasattr(self, "_close__fp"):
- self._close__fp()
+ if getattr(self, "_fp", False):
+ if self._fp != self.fp:
+ self._fp.close()
+ self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
self.fp = None
@@ -563,8 +570,10 @@ class Image:
more information.
"""
try:
- if hasattr(self, "_close__fp"):
- self._close__fp()
+ if getattr(self, "_fp", False):
+ if self._fp != self.fp:
+ self._fp.close()
+ self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp:
self.fp.close()
self.fp = None
@@ -1324,7 +1333,7 @@ class Image:
def getextrema(self):
"""
- Gets the the minimum and maximum pixel values for each band in
+ Gets the minimum and maximum pixel values for each band in
the image.
:returns: For a single-band image, a 2-tuple containing the
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 86a8ad5af..681b75d44 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -711,8 +711,13 @@ class FreeTypeFont:
:return: A FreeTypeFont object.
"""
+ if font is None:
+ try:
+ font = BytesIO(self.font_bytes)
+ except AttributeError:
+ font = self.path
return FreeTypeFont(
- font=self.path if font is None else font,
+ font=font,
size=self.size if size is None else size,
index=self.index if index is None else index,
encoding=self.encoding if encoding is None else encoding,
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index 9248b1b65..d4f6c90f7 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -62,7 +62,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
if not self.images:
raise SyntaxError("not an MIC file; no image entries")
- self.__fp = self.fp
self.frame = None
self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1
@@ -89,15 +88,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
def tell(self):
return self.frame
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
#
# --------------------------------------------------------------------
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 88c1bfcc5..fc3f8556f 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -58,20 +58,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
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._fp = self.fp # FIXME: hack
+ self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
self.__frame = 0
self.offset = 0
# for now we can only handle reading and individual frame extraction
self.readonly = 1
def load_seek(self, pos):
- self.__fp.seek(pos)
+ self._fp.seek(pos)
def seek(self, frame):
if not self._seek_check(frame):
return
- self.fp = self.__fp
+ self.fp = self._fp
self.offset = self.__mpoffsets[frame]
self.fp.seek(self.offset + 2) # skip SOI marker
@@ -97,15 +97,6 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
def tell(self):
return self.__frame
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
@staticmethod
def adopt(jpeg_instance, mpheader=None):
"""
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 01b4fd9ce..c0b364788 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -710,7 +710,7 @@ class PngImageFile(ImageFile.ImageFile):
if not _accept(self.fp.read(8)):
raise SyntaxError("not a PNG file")
- self.__fp = self.fp
+ self._fp = self.fp
self.__frame = 0
#
@@ -767,7 +767,7 @@ class PngImageFile(ImageFile.ImageFile):
self._close_exclusive_fp_after_loading = False
self.png.save_rewind()
self.__rewind_idat = self.__prepare_idat
- self.__rewind = self.__fp.tell()
+ self.__rewind = self._fp.tell()
if self.default_image:
# IDAT chunk contains default image and not first animation frame
self.n_frames += 1
@@ -822,7 +822,7 @@ class PngImageFile(ImageFile.ImageFile):
def _seek(self, frame, rewind=False):
if frame == 0:
if rewind:
- self.__fp.seek(self.__rewind)
+ self._fp.seek(self.__rewind)
self.png.rewind()
self.__prepare_idat = self.__rewind_idat
self.im = None
@@ -830,7 +830,7 @@ class PngImageFile(ImageFile.ImageFile):
self.pyaccess = None
self.info = self.png.im_info
self.tile = self.png.im_tile
- self.fp = self.__fp
+ self.fp = self._fp
self._prev_im = None
self.dispose = None
self.default_image = self.info.get("default_image", False)
@@ -849,7 +849,7 @@ class PngImageFile(ImageFile.ImageFile):
self.im.paste(self.dispose, self.dispose_extent)
self._prev_im = self.im.copy()
- self.fp = self.__fp
+ self.fp = self._fp
# advance to the next frame
if self.__prepare_idat:
@@ -1027,15 +1027,6 @@ class PngImageFile(ImageFile.ImageFile):
else {}
)
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
# --------------------------------------------------------------------
# PNG writer
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index dd755ed15..04c2e4fe3 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -132,7 +132,7 @@ class PsdImageFile(ImageFile.ImageFile):
self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels)
# keep the file open
- self.__fp = self.fp
+ self._fp = self.fp
self.frame = 1
self._min_frame = 1
@@ -146,7 +146,7 @@ class PsdImageFile(ImageFile.ImageFile):
self.mode = mode
self.tile = tile
self.frame = layer
- self.fp = self.__fp
+ self.fp = self._fp
return name, bbox
except IndexError as e:
raise EOFError("no such layer") from e
@@ -155,15 +155,6 @@ class PsdImageFile(ImageFile.ImageFile):
# return layer number (0=image, 1..max=layers)
return self.frame
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
def _layerinfo(fp, ct_bytes):
# read layerinfo block
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 1a72f5c04..acafc320e 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -15,7 +15,7 @@
#
##
-# Image plugin for the Spider image format. This format is is used
+# Image plugin for the Spider image format. This format is used
# by the SPIDER software, in processing image data from electron
# microscopy and tomography.
##
@@ -149,7 +149,7 @@ class SpiderImageFile(ImageFile.ImageFile):
self.mode = "F"
self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))]
- self.__fp = self.fp # FIXME: hack
+ self._fp = self.fp # FIXME: hack
@property
def n_frames(self):
@@ -172,7 +172,7 @@ class SpiderImageFile(ImageFile.ImageFile):
if not self._seek_check(frame):
return
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
- self.fp = self.__fp
+ self.fp = self._fp
self.fp.seek(self.stkoffset)
self._open()
@@ -191,15 +191,6 @@ class SpiderImageFile(ImageFile.ImageFile):
return ImageTk.PhotoImage(self.convert2byte(), palette=256)
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
# --------------------------------------------------------------------
# Image series
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 99d15e649..7cfd76af0 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1073,7 +1073,7 @@ class TiffImageFile(ImageFile.ImageFile):
# setup frame pointers
self.__first = self.__next = self.tag_v2.next
self.__frame = -1
- self.__fp = self.fp
+ self._fp = self.fp
self._frame_pos = []
self._n_frames = None
@@ -1106,7 +1106,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.im = Image.core.new(self.mode, self.size)
def _seek(self, frame):
- self.fp = self.__fp
+ self.fp = self._fp
# reset buffered io handle in case fp
# was passed to libtiff, invalidating the buffer
@@ -1515,15 +1515,6 @@ class TiffImageFile(ImageFile.ImageFile):
self._tile_orientation = self.tag_v2.get(0x0112)
- def _close__fp(self):
- try:
- if self.__fp != self.fp:
- self.__fp.close()
- except AttributeError:
- pass
- finally:
- self.__fp = None
-
#
# --------------------------------------------------------------------
@@ -1568,7 +1559,13 @@ def _save(im, fp, filename):
encoderinfo = im.encoderinfo
encoderconfig = im.encoderconfig
- compression = encoderinfo.get("compression", im.info.get("compression"))
+ try:
+ compression = encoderinfo["compression"]
+ except KeyError:
+ compression = im.info.get("compression")
+ if isinstance(compression, int):
+ # compression value may be from BMP. Ignore it
+ compression = None
if compression is None:
compression = "raw"
elif compression == "tiff_jpeg":
diff --git a/src/libImaging/GifDecode.c b/src/libImaging/GifDecode.c
index 0be4771cd..92b2607b4 100644
--- a/src/libImaging/GifDecode.c
+++ b/src/libImaging/GifDecode.c
@@ -125,7 +125,7 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t
context->blocksize--;
- /* New bits are shifted in from from the left. */
+ /* New bits are shifted in from the left. */
context->bitbuffer |= (INT32)c << context->bitcount;
context->bitcount += 8;
diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c
index 1c6b9d6a2..dfa6d842d 100644
--- a/src/libImaging/Quant.c
+++ b/src/libImaging/Quant.c
@@ -1519,7 +1519,7 @@ error_0:
typedef struct {
Pixel new;
- Pixel furthest;
+ uint32_t furthestV;
uint32_t furthestDistance;
int secondPixel;
} DistanceData;
@@ -1536,7 +1536,7 @@ compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u
}
if (oldDist > data->furthestDistance) {
data->furthestDistance = oldDist;
- data->furthest.v = pixel.v;
+ data->furthestV = pixel.v;
}
}
@@ -1577,10 +1577,11 @@ quantize2(
data.new.c.b = (int)(.5 + (double)mean[2] / (double)nPixels);
for (i = 0; i < nQuantPixels; i++) {
data.furthestDistance = 0;
+ data.furthestV = pixelData[0].v;
data.secondPixel = (i == 1) ? 1 : 0;
hashtable_foreach_update(h, compute_distances, &data);
- p[i].v = data.furthest.v;
- data.new.v = data.furthest.v;
+ p[i].v = data.furthestV;
+ data.new.v = data.furthestV;
}
hashtable_free(h);
diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c
index df430c940..95ae9b622 100644
--- a/src/libImaging/TgaRleDecode.c
+++ b/src/libImaging/TgaRleDecode.c
@@ -120,6 +120,7 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
}
memcpy(state->buffer + state->x, ptr, n);
ptr += n;
+ bytes -= n;
extra_bytes -= n;
}
}
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index c5fcd62ff..3d9391321 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -246,15 +246,15 @@ deps = {
"libs": [r"Lib\MS\*.lib"],
},
"openjpeg": {
- "url": "https://github.com/uclouvain/openjpeg/archive/v2.4.0.tar.gz",
- "filename": "openjpeg-2.4.0.tar.gz",
- "dir": "openjpeg-2.4.0",
+ "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz",
+ "filename": "openjpeg-2.5.0.tar.gz",
+ "dir": "openjpeg-2.5.0",
"build": [
cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")),
cmd_nmake(target="clean"),
cmd_nmake(target="openjp2"),
- cmd_mkdir(r"{inc_dir}\openjpeg-2.4.0"),
- cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.4.0"),
+ cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"),
+ cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"),
],
"libs": [r"bin\*.lib"],
},
@@ -280,9 +280,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/4.2.1.zip",
- "filename": "harfbuzz-4.2.1.zip",
- "dir": "harfbuzz-4.2.1",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/4.3.0.zip",
+ "filename": "harfbuzz-4.3.0.zip",
+ "dir": "harfbuzz-4.3.0",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),