mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-06-29 09:23:11 +03:00
Merge branch 'main' into comment_correct_placement
This commit is contained in:
commit
db76eaa12c
12
.github/workflows/test-docker.yml
vendored
12
.github/workflows/test-docker.yml
vendored
|
@ -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 }}
|
||||||
|
|
30
CHANGES.rst
30
CHANGES.rst
|
@ -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)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
BIN
Tests/images/cross_scan_line_truncated.tga
Normal file
BIN
Tests/images/cross_scan_line_truncated.tga
Normal file
Binary file not shown.
BIN
Tests/images/multiple_comments.gif
Normal file
BIN
Tests/images/multiple_comments.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -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_",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
|
|
|
@ -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 |
|
||||||
+----------------------------------+----------------------------+---------------------+
|
+----------------------------------+----------------------------+---------------------+
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
16
docs/releasenotes/9.1.1.rst
Normal file
16
docs/releasenotes/9.1.1.rst
Normal 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``.
|
|
@ -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
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user