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"),