Merge branch 'main' into comment_correct_placement

This commit is contained in:
Andrew Murray 2022-05-22 14:56:57 +10:00 committed by GitHub
commit db76eaa12c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 290 additions and 204 deletions

View File

@ -11,9 +11,9 @@ jobs:
matrix: matrix:
docker: [ docker: [
# Run slower jobs first to give them a headstart and reduce waiting time # Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-20.04-focal-arm64v8, ubuntu-22.04-jammy-arm64v8,
ubuntu-20.04-focal-ppc64le, ubuntu-22.04-jammy-ppc64le,
ubuntu-20.04-focal-s390x, ubuntu-22.04-jammy-s390x,
# Then run the remainder # Then run the remainder
alpine, alpine,
amazon-2-amd64, amazon-2-amd64,
@ -32,11 +32,11 @@ jobs:
] ]
dockerTag: [main] dockerTag: [main]
include: include:
- docker: "ubuntu-20.04-focal-arm64v8" - docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64" qemu-arch: "aarch64"
- docker: "ubuntu-20.04-focal-ppc64le" - docker: "ubuntu-22.04-jammy-ppc64le"
qemu-arch: "ppc64le" qemu-arch: "ppc64le"
- docker: "ubuntu-20.04-focal-s390x" - docker: "ubuntu-22.04-jammy-s390x"
qemu-arch: "s390x" qemu-arch: "s390x"
name: ${{ matrix.docker }} name: ${{ matrix.docker }}

View File

@ -5,6 +5,24 @@ Changelog (Pillow)
9.2.0 (unreleased) 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 - Populate Python palette in fromarray() #6283
[radarhere] [radarhere]
@ -17,9 +35,6 @@ Changelog (Pillow)
- Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270 - Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270
[radarhere] [radarhere]
- Do not open images with zero or negative height #6269
[radarhere]
- Search pkgconf system libs/cflags #6138 - Search pkgconf system libs/cflags #6138
[jameshilliard, radarhere] [jameshilliard, radarhere]
@ -50,6 +65,15 @@ Changelog (Pillow)
- Deprecated PhotoImage.paste() box parameter #6178 - Deprecated PhotoImage.paste() box parameter #6178
[radarhere] [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) 9.1.0 (2022-04-01)
------------------ ------------------

View File

@ -85,6 +85,8 @@ release-test:
sdist: sdist:
python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
python3 -m build --sdist 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 .PHONY: test
test: test:

View File

@ -24,7 +24,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Create and check source distribution: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] 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.: * [ ] 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] 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.: * [ ] 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: * [ ] Create and check source distribution:
```bash ```bash
make sdist make sdist
python3 -m twine check --strict dist/*
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] 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) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -637,6 +637,15 @@ def test_apng_save_blend(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255) 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(): def test_constants_deprecation():
for enum, prefix in { for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_", PngImagePlugin.Disposal: "APNG_DISPOSE_",

View File

@ -46,6 +46,15 @@ def test_closed_file():
im.close() 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(): def test_context_manager():
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:

View File

@ -46,6 +46,19 @@ def test_closed_file():
im.close() 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(): def test_context_manager():
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:
@ -794,6 +807,9 @@ def test_comment(tmp_path):
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["comment"] == im.info["comment"].encode() 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): def test_comment_over_255(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -804,15 +820,23 @@ def test_comment_over_255(tmp_path):
im.info["comment"] = comment im.info["comment"] = comment
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["comment"] == comment assert reread.info["comment"] == comment
# Test that GIF89a is used for comments
assert reread.info["version"] == b"GIF89a"
def test_zero_comment_subblocks(): def test_zero_comment_subblocks():
with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
assert_image_equal_tofile(im, TEST_GIF) 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): def test_write_comment(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/dispose_prev.gif") as im: with Image.open("Tests/images/dispose_prev.gif") as im:

View File

@ -18,6 +18,7 @@ from .helper import (
hopper, hopper,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
skip_unless_feature_version,
) )
@ -991,6 +992,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im: with Image.open(out) as im:
im.load() im.load()
@skip_unless_feature_version("libtiff", "4.0.4")
def test_realloc_overflow(self): def test_realloc_overflow(self):
TiffImagePlugin.READ_LIBTIFF = True TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:

View File

@ -48,6 +48,14 @@ def test_closed_file():
im.close() 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(): def test_context_manager():
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open(test_files[0]) as im: with Image.open(test_files[0]) as im:

View File

@ -101,6 +101,10 @@ def test_cross_scan_line():
with Image.open("Tests/images/cross_scan_line.tga") as im: with Image.open("Tests/images/cross_scan_line.tga") as im:
assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") 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): def test_save(tmp_path):
test_file = "Tests/images/tga_id_field.tga" test_file = "Tests/images/tga_id_field.tga"

View File

@ -70,6 +70,15 @@ class TestFileTiff:
im.load() im.load()
im.close() 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): def test_context_manager(self):
with warnings.catch_warnings(): with warnings.catch_warnings():
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
@ -706,6 +715,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert reloaded.info["icc_profile"] == icc_profile 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): def test_discard_icc_profile(self, tmp_path):
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")

View File

@ -1,5 +1,7 @@
import pytest import pytest
from PIL import Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
@ -17,11 +19,24 @@ def test_sanity():
im.point(list(range(256))) 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)
im.point(lambda x: x - 1)
im.point(lambda x: x * 1 + 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): with pytest.raises(TypeError):
im.point(lambda x: x - 1) im.point(lambda x: x * x)
with pytest.raises(TypeError): 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(): def test_16bit_lut():
@ -47,3 +62,8 @@ def test_f_mode():
im = hopper("F") im = hopper("F")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.point(None) im.point(None)
def test_coerce_e_deprecation():
with pytest.warns(DeprecationWarning):
assert Image.coerce_e(2).data == 2

View File

@ -65,9 +65,12 @@ class TestImageFont:
return font_bytes return font_bytes
def test_font_with_filelike(self): def test_font_with_filelike(self):
ImageFont.truetype( ttf = ImageFont.truetype(
self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE 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()) self._render(self._font_as_bytes())
# Usage note: making two fonts from the same buffer fails. # Usage note: making two fonts from the same buffer fails.
# shared_bytes = self._font_as_bytes() # shared_bytes = self._font_as_bytes()

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install openjpeg # 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 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -170,6 +170,14 @@ in Pillow 10 (2023-07-01). Upgrade to
`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or `PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
`PySide6 <https://doc.qt.io/qtforpython/>`_ instead. `PySide6 <https://doc.qt.io/qtforpython/>`_ 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 Removed features
---------------- ----------------

View File

@ -181,7 +181,8 @@ Many of Pillow's features require external libraries:
* **openjpeg** provides JPEG 2000 functionality. * **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 * Pillow does **not** support the earlier **1.5** series which ships
with Debian Jessie. 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 | | Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | PyPy3 | | | | 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 | | Windows Server 2016 | 3.7 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+

View File

@ -174,7 +174,7 @@ Previously, if a BMP file was too large, an ``OSError`` would be raised. Now,
Dark theme for docs 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.

View File

@ -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``.

View File

@ -31,6 +31,14 @@ FreeTypeFont.getmask2 fill parameter
The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2`
has been deprecated and will be removed in Pillow 10 (2023-07-01). 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 API Changes
=========== ===========

View File

@ -15,6 +15,7 @@ expected to be backported to earlier versions.
:maxdepth: 2 :maxdepth: 2
9.2.0 9.2.0
9.1.1
9.1.0 9.1.0
9.0.1 9.0.1
9.0.0 9.0.0

View File

@ -57,7 +57,7 @@ class DcxImageFile(PcxImageFile):
break break
self._offset.append(offset) self._offset.append(offset)
self.__fp = self.fp self._fp = self.fp
self.frame = None self.frame = None
self.n_frames = len(self._offset) self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
@ -67,22 +67,13 @@ class DcxImageFile(PcxImageFile):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.frame = frame self.frame = frame
self.fp = self.__fp self.fp = self._fp
self.fp.seek(self._offset[frame]) self.fp.seek(self._offset[frame])
PcxImageFile._open(self) PcxImageFile._open(self)
def tell(self): def tell(self):
return self.frame 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) Image.register_open(DcxImageFile.format, DcxImageFile, _accept)

View File

@ -91,7 +91,7 @@ class FliImageFile(ImageFile.ImageFile):
# set things up to decode first frame # set things up to decode first frame
self.__frame = -1 self.__frame = -1
self.__fp = self.fp self._fp = self.fp
self.__rewind = self.fp.tell() self.__rewind = self.fp.tell()
self.seek(0) self.seek(0)
@ -125,7 +125,7 @@ class FliImageFile(ImageFile.ImageFile):
def _seek(self, frame): def _seek(self, frame):
if frame == 0: if frame == 0:
self.__frame = -1 self.__frame = -1
self.__fp.seek(self.__rewind) self._fp.seek(self.__rewind)
self.__offset = 128 self.__offset = 128
else: else:
# ensure that the previous frame was loaded # ensure that the previous frame was loaded
@ -136,7 +136,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__frame = frame self.__frame = frame
# move to next frame # move to next frame
self.fp = self.__fp self.fp = self._fp
self.fp.seek(self.__offset) self.fp.seek(self.__offset)
s = self.fp.read(4) s = self.fp.read(4)
@ -153,15 +153,6 @@ class FliImageFile(ImageFile.ImageFile):
def tell(self): def tell(self):
return self.__frame return self.__frame
def _close__fp(self):
try:
if self.__fp != self.fp:
self.__fp.close()
except AttributeError:
pass
finally:
self.__fp = None
# #
# registry # registry

View File

@ -102,7 +102,7 @@ class GifImageFile(ImageFile.ImageFile):
p = ImagePalette.raw("RGB", p) p = ImagePalette.raw("RGB", p)
self.global_palette = self.palette = 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.__rewind = self.fp.tell()
self._n_frames = None self._n_frames = None
self._is_animated = None self._is_animated = None
@ -161,7 +161,7 @@ class GifImageFile(ImageFile.ImageFile):
self.__offset = 0 self.__offset = 0
self.dispose = None self.dispose = None
self.__frame = -1 self.__frame = -1
self.__fp.seek(self.__rewind) self._fp.seek(self.__rewind)
self.disposal_method = 0 self.disposal_method = 0
else: else:
# ensure that the previous frame was loaded # ensure that the previous frame was loaded
@ -171,7 +171,7 @@ class GifImageFile(ImageFile.ImageFile):
if frame != self.__frame + 1: if frame != self.__frame + 1:
raise ValueError(f"cannot seek to frame {frame}") raise ValueError(f"cannot seek to frame {frame}")
self.fp = self.__fp self.fp = self._fp
if self.__offset: if self.__offset:
# backup to last frame # backup to last frame
self.fp.seek(self.__offset) self.fp.seek(self.__offset)
@ -228,12 +228,18 @@ class GifImageFile(ImageFile.ImageFile):
# #
# comment extension # comment extension
# #
comment = b""
# Collect one comment block
while block: while block:
if "comment" in info: comment += block
info["comment"] += block
else:
info["comment"] = block
block = self.data() 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 s = None
continue continue
elif s[0] == 255: elif s[0] == 255:
@ -281,7 +287,7 @@ class GifImageFile(ImageFile.ImageFile):
s = None s = None
if interlace is None: if interlace is None:
# self.__fp = None # self._fp = None
raise EOFError raise EOFError
if not update_image: if not update_image:
return return
@ -443,15 +449,6 @@ class GifImageFile(ImageFile.ImageFile):
def tell(self): def tell(self):
return self.__frame 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 # Write GIF files
@ -903,17 +900,16 @@ def _get_global_header(im, info):
# https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
version = b"87a" version = b"87a"
for extensionKey in ["transparency", "duration", "loop", "comment"]: if im.info.get("version") == b"89a" or (
if info and extensionKey in info: info
if (extensionKey == "duration" and info[extensionKey] == 0) or ( and (
extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255) "transparency" in info
): or "loop" in info
continue or info.get("duration")
version = b"89a" or info.get("comment")
break )
else: ):
if im.info.get("version") == b"89a": version = b"89a"
version = b"89a"
background = _get_background(im, info.get("background")) background = _get_background(im, info.get("background"))

View File

@ -245,7 +245,7 @@ class ImImageFile(ImageFile.ImageFile):
self.__offset = offs = self.fp.tell() self.__offset = offs = self.fp.tell()
self.__fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
if self.rawmode[:2] == "F;": if self.rawmode[:2] == "F;":
@ -294,22 +294,13 @@ class ImImageFile(ImageFile.ImageFile):
size = ((self.size[0] * bits + 7) // 8) * self.size[1] size = ((self.size[0] * bits + 7) // 8) * self.size[1]
offs = self.__offset + frame * size 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))] self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
def tell(self): def tell(self):
return self.frame return self.frame
def _close__fp(self):
try:
if self.__fp != self.fp:
self.__fp.close()
except AttributeError:
pass
finally:
self.__fp = None
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -29,7 +29,6 @@ import builtins
import io import io
import logging import logging
import math import math
import numbers
import os import os
import re import re
import struct import struct
@ -432,44 +431,50 @@ def _getencoder(mode, encoder_name, args, extra=()):
def coerce_e(value): 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: class _E:
def __init__(self, data): def __init__(self, scale, data):
self.scale = scale
self.data = data self.data = data
def __neg__(self):
return _E(-self.scale, -self.data)
def __add__(self, other): 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): 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): def _getscaleoffset(expr):
stub = ["stub"] a = expr(_E(1, 0))
data = expr(_E(stub)).data return (a.scale, a.data) if isinstance(a, _E) else (0, a)
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")
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -544,8 +549,10 @@ class Image:
def __exit__(self, *args): def __exit__(self, *args):
if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False):
if hasattr(self, "_close__fp"): if getattr(self, "_fp", False):
self._close__fp() if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp: if self.fp:
self.fp.close() self.fp.close()
self.fp = None self.fp = None
@ -563,8 +570,10 @@ class Image:
more information. more information.
""" """
try: try:
if hasattr(self, "_close__fp"): if getattr(self, "_fp", False):
self._close__fp() if self._fp != self.fp:
self._fp.close()
self._fp = DeferredError(ValueError("Operation on closed image"))
if self.fp: if self.fp:
self.fp.close() self.fp.close()
self.fp = None self.fp = None
@ -1324,7 +1333,7 @@ class Image:
def getextrema(self): 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. the image.
:returns: For a single-band image, a 2-tuple containing the :returns: For a single-band image, a 2-tuple containing the

View File

@ -711,8 +711,13 @@ class FreeTypeFont:
:return: A FreeTypeFont object. :return: A FreeTypeFont object.
""" """
if font is None:
try:
font = BytesIO(self.font_bytes)
except AttributeError:
font = self.path
return FreeTypeFont( return FreeTypeFont(
font=self.path if font is None else font, font=font,
size=self.size if size is None else size, size=self.size if size is None else size,
index=self.index if index is None else index, index=self.index if index is None else index,
encoding=self.encoding if encoding is None else encoding, encoding=self.encoding if encoding is None else encoding,

View File

@ -62,7 +62,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
if not self.images: if not self.images:
raise SyntaxError("not an MIC file; no image entries") raise SyntaxError("not an MIC file; no image entries")
self.__fp = self.fp
self.frame = None self.frame = None
self._n_frames = len(self.images) self._n_frames = len(self.images)
self.is_animated = self._n_frames > 1 self.is_animated = self._n_frames > 1
@ -89,15 +88,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
def tell(self): def tell(self):
return self.frame return self.frame
def _close__fp(self):
try:
if self.__fp != self.fp:
self.__fp.close()
except AttributeError:
pass
finally:
self.__fp = None
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -58,20 +58,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
assert self.n_frames == len(self.__mpoffsets) assert self.n_frames == len(self.__mpoffsets)
del self.info["mpoffset"] # no longer needed del self.info["mpoffset"] # no longer needed
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
self.__fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
self.__frame = 0 self.__frame = 0
self.offset = 0 self.offset = 0
# for now we can only handle reading and individual frame extraction # for now we can only handle reading and individual frame extraction
self.readonly = 1 self.readonly = 1
def load_seek(self, pos): def load_seek(self, pos):
self.__fp.seek(pos) self._fp.seek(pos)
def seek(self, frame): def seek(self, frame):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.fp = self.__fp self.fp = self._fp
self.offset = self.__mpoffsets[frame] self.offset = self.__mpoffsets[frame]
self.fp.seek(self.offset + 2) # skip SOI marker self.fp.seek(self.offset + 2) # skip SOI marker
@ -97,15 +97,6 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
def tell(self): def tell(self):
return self.__frame return self.__frame
def _close__fp(self):
try:
if self.__fp != self.fp:
self.__fp.close()
except AttributeError:
pass
finally:
self.__fp = None
@staticmethod @staticmethod
def adopt(jpeg_instance, mpheader=None): def adopt(jpeg_instance, mpheader=None):
""" """

View File

@ -710,7 +710,7 @@ class PngImageFile(ImageFile.ImageFile):
if not _accept(self.fp.read(8)): if not _accept(self.fp.read(8)):
raise SyntaxError("not a PNG file") raise SyntaxError("not a PNG file")
self.__fp = self.fp self._fp = self.fp
self.__frame = 0 self.__frame = 0
# #
@ -767,7 +767,7 @@ class PngImageFile(ImageFile.ImageFile):
self._close_exclusive_fp_after_loading = False self._close_exclusive_fp_after_loading = False
self.png.save_rewind() self.png.save_rewind()
self.__rewind_idat = self.__prepare_idat self.__rewind_idat = self.__prepare_idat
self.__rewind = self.__fp.tell() self.__rewind = self._fp.tell()
if self.default_image: if self.default_image:
# IDAT chunk contains default image and not first animation frame # IDAT chunk contains default image and not first animation frame
self.n_frames += 1 self.n_frames += 1
@ -822,7 +822,7 @@ class PngImageFile(ImageFile.ImageFile):
def _seek(self, frame, rewind=False): def _seek(self, frame, rewind=False):
if frame == 0: if frame == 0:
if rewind: if rewind:
self.__fp.seek(self.__rewind) self._fp.seek(self.__rewind)
self.png.rewind() self.png.rewind()
self.__prepare_idat = self.__rewind_idat self.__prepare_idat = self.__rewind_idat
self.im = None self.im = None
@ -830,7 +830,7 @@ class PngImageFile(ImageFile.ImageFile):
self.pyaccess = None self.pyaccess = None
self.info = self.png.im_info self.info = self.png.im_info
self.tile = self.png.im_tile self.tile = self.png.im_tile
self.fp = self.__fp self.fp = self._fp
self._prev_im = None self._prev_im = None
self.dispose = None self.dispose = None
self.default_image = self.info.get("default_image", False) 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.im.paste(self.dispose, self.dispose_extent)
self._prev_im = self.im.copy() self._prev_im = self.im.copy()
self.fp = self.__fp self.fp = self._fp
# advance to the next frame # advance to the next frame
if self.__prepare_idat: if self.__prepare_idat:
@ -1027,15 +1027,6 @@ class PngImageFile(ImageFile.ImageFile):
else {} else {}
) )
def _close__fp(self):
try:
if self.__fp != self.fp:
self.__fp.close()
except AttributeError:
pass
finally:
self.__fp = None
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# PNG writer # PNG writer

View File

@ -132,7 +132,7 @@ class PsdImageFile(ImageFile.ImageFile):
self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels)
# keep the file open # keep the file open
self.__fp = self.fp self._fp = self.fp
self.frame = 1 self.frame = 1
self._min_frame = 1 self._min_frame = 1
@ -146,7 +146,7 @@ class PsdImageFile(ImageFile.ImageFile):
self.mode = mode self.mode = mode
self.tile = tile self.tile = tile
self.frame = layer self.frame = layer
self.fp = self.__fp self.fp = self._fp
return name, bbox return name, bbox
except IndexError as e: except IndexError as e:
raise EOFError("no such layer") from 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 layer number (0=image, 1..max=layers)
return self.frame 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): def _layerinfo(fp, ct_bytes):
# read layerinfo block # read layerinfo block

View File

@ -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 # by the SPIDER software, in processing image data from electron
# microscopy and tomography. # microscopy and tomography.
## ##
@ -149,7 +149,7 @@ class SpiderImageFile(ImageFile.ImageFile):
self.mode = "F" self.mode = "F"
self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] 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 @property
def n_frames(self): def n_frames(self):
@ -172,7 +172,7 @@ class SpiderImageFile(ImageFile.ImageFile):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
self.fp = self.__fp self.fp = self._fp
self.fp.seek(self.stkoffset) self.fp.seek(self.stkoffset)
self._open() self._open()
@ -191,15 +191,6 @@ class SpiderImageFile(ImageFile.ImageFile):
return ImageTk.PhotoImage(self.convert2byte(), palette=256) 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 # Image series

View File

@ -1073,7 +1073,7 @@ class TiffImageFile(ImageFile.ImageFile):
# setup frame pointers # setup frame pointers
self.__first = self.__next = self.tag_v2.next self.__first = self.__next = self.tag_v2.next
self.__frame = -1 self.__frame = -1
self.__fp = self.fp self._fp = self.fp
self._frame_pos = [] self._frame_pos = []
self._n_frames = None self._n_frames = None
@ -1106,7 +1106,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
def _seek(self, frame): def _seek(self, frame):
self.fp = self.__fp self.fp = self._fp
# reset buffered io handle in case fp # reset buffered io handle in case fp
# was passed to libtiff, invalidating the buffer # was passed to libtiff, invalidating the buffer
@ -1515,15 +1515,6 @@ class TiffImageFile(ImageFile.ImageFile):
self._tile_orientation = self.tag_v2.get(0x0112) 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 encoderinfo = im.encoderinfo
encoderconfig = im.encoderconfig 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: if compression is None:
compression = "raw" compression = "raw"
elif compression == "tiff_jpeg": elif compression == "tiff_jpeg":

View File

@ -125,7 +125,7 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t
context->blocksize--; 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->bitbuffer |= (INT32)c << context->bitcount;
context->bitcount += 8; context->bitcount += 8;

View File

@ -1519,7 +1519,7 @@ error_0:
typedef struct { typedef struct {
Pixel new; Pixel new;
Pixel furthest; uint32_t furthestV;
uint32_t furthestDistance; uint32_t furthestDistance;
int secondPixel; int secondPixel;
} DistanceData; } DistanceData;
@ -1536,7 +1536,7 @@ compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u
} }
if (oldDist > data->furthestDistance) { if (oldDist > data->furthestDistance) {
data->furthestDistance = oldDist; 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); data.new.c.b = (int)(.5 + (double)mean[2] / (double)nPixels);
for (i = 0; i < nQuantPixels; i++) { for (i = 0; i < nQuantPixels; i++) {
data.furthestDistance = 0; data.furthestDistance = 0;
data.furthestV = pixelData[0].v;
data.secondPixel = (i == 1) ? 1 : 0; data.secondPixel = (i == 1) ? 1 : 0;
hashtable_foreach_update(h, compute_distances, &data); hashtable_foreach_update(h, compute_distances, &data);
p[i].v = data.furthest.v; p[i].v = data.furthestV;
data.new.v = data.furthest.v; data.new.v = data.furthestV;
} }
hashtable_free(h); hashtable_free(h);

View File

@ -120,6 +120,7 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t
} }
memcpy(state->buffer + state->x, ptr, n); memcpy(state->buffer + state->x, ptr, n);
ptr += n; ptr += n;
bytes -= n;
extra_bytes -= n; extra_bytes -= n;
} }
} }

View File

@ -246,15 +246,15 @@ deps = {
"libs": [r"Lib\MS\*.lib"], "libs": [r"Lib\MS\*.lib"],
}, },
"openjpeg": { "openjpeg": {
"url": "https://github.com/uclouvain/openjpeg/archive/v2.4.0.tar.gz", "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz",
"filename": "openjpeg-2.4.0.tar.gz", "filename": "openjpeg-2.5.0.tar.gz",
"dir": "openjpeg-2.4.0", "dir": "openjpeg-2.5.0",
"build": [ "build": [
cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")),
cmd_nmake(target="clean"), cmd_nmake(target="clean"),
cmd_nmake(target="openjp2"), cmd_nmake(target="openjp2"),
cmd_mkdir(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.4.0"), cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"),
], ],
"libs": [r"bin\*.lib"], "libs": [r"bin\*.lib"],
}, },
@ -280,9 +280,9 @@ deps = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.2.1.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/4.3.0.zip",
"filename": "harfbuzz-4.2.1.zip", "filename": "harfbuzz-4.3.0.zip",
"dir": "harfbuzz-4.2.1", "dir": "harfbuzz-4.3.0",
"build": [ "build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"), cmd_nmake(target="clean"),