mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-04 20:03:20 +03:00
Merge branch 'main' into image_equals
This commit is contained in:
commit
198e3730be
|
@ -20,7 +20,7 @@ fi
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||||
sway wl-clipboard libopenblas-dev
|
sway wl-clipboard libopenblas-dev
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==2.23.0
|
cibuildwheel==2.23.1
|
||||||
|
|
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
|
@ -16,6 +16,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"schedule": [
|
"schedule": [
|
||||||
"on the 3rd day of the month"
|
"* * 3 * *"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
13
.github/workflows/test-docker.yml
vendored
13
.github/workflows/test-docker.yml
vendored
|
@ -35,6 +35,10 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
os: ["ubuntu-latest"]
|
os: ["ubuntu-latest"]
|
||||||
docker: [
|
docker: [
|
||||||
|
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||||
|
ubuntu-24.04-noble-ppc64le,
|
||||||
|
ubuntu-24.04-noble-s390x,
|
||||||
|
# Then run the remainder
|
||||||
alpine,
|
alpine,
|
||||||
amazon-2-amd64,
|
amazon-2-amd64,
|
||||||
amazon-2023-amd64,
|
amazon-2023-amd64,
|
||||||
|
@ -52,13 +56,9 @@ jobs:
|
||||||
dockerTag: [main]
|
dockerTag: [main]
|
||||||
include:
|
include:
|
||||||
- docker: "ubuntu-24.04-noble-ppc64le"
|
- docker: "ubuntu-24.04-noble-ppc64le"
|
||||||
os: "ubuntu-22.04"
|
|
||||||
qemu-arch: "ppc64le"
|
qemu-arch: "ppc64le"
|
||||||
dockerTag: main
|
|
||||||
- docker: "ubuntu-24.04-noble-s390x"
|
- docker: "ubuntu-24.04-noble-s390x"
|
||||||
os: "ubuntu-22.04"
|
|
||||||
qemu-arch: "s390x"
|
qemu-arch: "s390x"
|
||||||
dockerTag: main
|
|
||||||
- docker: "ubuntu-24.04-noble-arm64v8"
|
- docker: "ubuntu-24.04-noble-arm64v8"
|
||||||
os: "ubuntu-24.04-arm"
|
os: "ubuntu-24.04-arm"
|
||||||
dockerTag: main
|
dockerTag: main
|
||||||
|
@ -75,8 +75,9 @@ jobs:
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
if: "matrix.qemu-arch"
|
if: "matrix.qemu-arch"
|
||||||
run: |
|
uses: docker/setup-qemu-action@v3
|
||||||
docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }}
|
with:
|
||||||
|
platforms: ${{ matrix.qemu-arch }}
|
||||||
|
|
||||||
- name: Docker pull
|
- name: Docker pull
|
||||||
run: |
|
run: |
|
||||||
|
|
4
.github/workflows/test-windows.yml
vendored
4
.github/workflows/test-windows.yml
vendored
|
@ -94,8 +94,8 @@ jobs:
|
||||||
choco install nasm --no-progress
|
choco install nasm --no-progress
|
||||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
choco install ghostscript --version=10.4.0 --no-progress
|
choco install ghostscript --version=10.5.0 --no-progress
|
||||||
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
|
echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
# Install extra test images
|
# Install extra test images
|
||||||
xcopy /S /Y Tests\test-images\* Tests\images
|
xcopy /S /Y Tests\test-images\* Tests\images
|
||||||
|
|
2
.github/workflows/wheels-dependencies.sh
vendored
2
.github/workflows/wheels-dependencies.sh
vendored
|
@ -43,7 +43,7 @@ LIBPNG_VERSION=1.6.47
|
||||||
JPEGTURBO_VERSION=3.1.0
|
JPEGTURBO_VERSION=3.1.0
|
||||||
OPENJPEG_VERSION=2.5.3
|
OPENJPEG_VERSION=2.5.3
|
||||||
XZ_VERSION=5.6.4
|
XZ_VERSION=5.6.4
|
||||||
TIFF_VERSION=4.6.0
|
TIFF_VERSION=4.7.0
|
||||||
LCMS2_VERSION=2.17
|
LCMS2_VERSION=2.17
|
||||||
ZLIB_NG_VERSION=2.2.4
|
ZLIB_NG_VERSION=2.2.4
|
||||||
LIBWEBP_VERSION=1.5.0
|
LIBWEBP_VERSION=1.5.0
|
||||||
|
|
|
@ -34,8 +34,11 @@ def test_apng_basic() -> None:
|
||||||
with pytest.raises(EOFError):
|
with pytest.raises(EOFError):
|
||||||
im.seek(2)
|
im.seek(2)
|
||||||
|
|
||||||
# test rewind support
|
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
|
with pytest.raises(ValueError, match="cannot seek to frame 2"):
|
||||||
|
im._seek(2)
|
||||||
|
|
||||||
|
# test rewind support
|
||||||
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
|
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (255, 0, 0, 255)
|
assert im.getpixel((64, 32)) == (255, 0, 0, 255)
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -132,6 +133,15 @@ def test_eoferror() -> None:
|
||||||
im.seek(n_frames - 1)
|
im.seek(n_frames - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_frame_size() -> None:
|
||||||
|
with open(animated_test_file, "rb") as fp:
|
||||||
|
data = fp.read()
|
||||||
|
data = data[:6188]
|
||||||
|
with Image.open(io.BytesIO(data)) as im:
|
||||||
|
with pytest.raises(EOFError, match="missing frame size"):
|
||||||
|
im.seek(1)
|
||||||
|
|
||||||
|
|
||||||
def test_seek_tell() -> None:
|
def test_seek_tell() -> None:
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
layer_number = im.tell()
|
layer_number = im.tell()
|
||||||
|
@ -160,6 +170,9 @@ def test_seek() -> None:
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
|
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="cannot seek to frame 52"):
|
||||||
|
im._seek(52)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"test_file",
|
"test_file",
|
||||||
|
|
|
@ -410,6 +410,10 @@ def test_seek() -> None:
|
||||||
except EOFError:
|
except EOFError:
|
||||||
assert frame_count == 5
|
assert frame_count == 5
|
||||||
|
|
||||||
|
img.seek(0)
|
||||||
|
with pytest.raises(ValueError, match="cannot seek to frame 2"):
|
||||||
|
img._seek(2)
|
||||||
|
|
||||||
|
|
||||||
def test_seek_info() -> None:
|
def test_seek_info() -> None:
|
||||||
with Image.open("Tests/images/iss634.gif") as im:
|
with Image.open("Tests/images/iss634.gif") as im:
|
||||||
|
@ -601,7 +605,7 @@ def test_save_dispose(tmp_path: Path) -> None:
|
||||||
Image.new("L", (100, 100), "#111"),
|
Image.new("L", (100, 100), "#111"),
|
||||||
Image.new("L", (100, 100), "#222"),
|
Image.new("L", (100, 100), "#222"),
|
||||||
]
|
]
|
||||||
for method in range(0, 4):
|
for method in range(4):
|
||||||
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
|
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
|
||||||
with Image.open(out) as img:
|
with Image.open(out) as img:
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
|
|
|
@ -313,6 +313,18 @@ def test_rgba(ext: str) -> None:
|
||||||
assert im.mode == "RGBA"
|
assert im.mode == "RGBA"
|
||||||
|
|
||||||
|
|
||||||
|
def test_grayscale_four_channels() -> None:
|
||||||
|
with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp:
|
||||||
|
data = fp.read()
|
||||||
|
|
||||||
|
# Change color space to OPJ_CLRSPC_GRAY
|
||||||
|
data = data[:76] + b"\x11" + data[77:]
|
||||||
|
|
||||||
|
with Image.open(BytesIO(data)) as im:
|
||||||
|
im.load()
|
||||||
|
assert im.mode == "RGBA"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1140,11 +1140,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
monkeypatch.setattr(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:
|
||||||
with pytest.raises(OSError) as e:
|
|
||||||
im.load()
|
|
||||||
|
|
||||||
# Assert that the error code is IMAGING_CODEC_MEMORY
|
# Assert that the error code is IMAGING_CODEC_MEMORY
|
||||||
assert str(e.value) == "decoder error -9"
|
with pytest.raises(OSError, match="decoder error -9"):
|
||||||
|
im.load()
|
||||||
|
|
||||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -36,6 +37,28 @@ def test_sanity(tmp_path: Path) -> None:
|
||||||
im.save(f)
|
im.save(f)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_image_size() -> None:
|
||||||
|
with open("Tests/images/pil184.pcx", "rb") as fp:
|
||||||
|
data = fp.read()
|
||||||
|
data = data[:4] + b"\xff\xff" + data[6:]
|
||||||
|
|
||||||
|
b = io.BytesIO(data)
|
||||||
|
with pytest.raises(SyntaxError, match="bad PCX image size"):
|
||||||
|
with PcxImagePlugin.PcxImageFile(b):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_mode() -> None:
|
||||||
|
with open("Tests/images/pil184.pcx", "rb") as fp:
|
||||||
|
data = fp.read()
|
||||||
|
data = data[:3] + b"\xff" + data[4:]
|
||||||
|
|
||||||
|
b = io.BytesIO(data)
|
||||||
|
with pytest.raises(OSError, match="unknown PCX mode"):
|
||||||
|
with Image.open(b):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file() -> None:
|
def test_invalid_file() -> None:
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
|
|
@ -293,12 +293,10 @@ def test_header_token_too_long(tmp_path: Path) -> None:
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"P6\n 01234567890")
|
f.write(b"P6\n 01234567890")
|
||||||
|
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
|
||||||
with Image.open(path):
|
with Image.open(path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert str(e.value) == "Token too long in file header: 01234567890"
|
|
||||||
|
|
||||||
|
|
||||||
def test_truncated_file(tmp_path: Path) -> None:
|
def test_truncated_file(tmp_path: Path) -> None:
|
||||||
# Test EOF in header
|
# Test EOF in header
|
||||||
|
@ -306,12 +304,10 @@ def test_truncated_file(tmp_path: Path) -> None:
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"P6")
|
f.write(b"P6")
|
||||||
|
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError, match="Reached EOF while reading header"):
|
||||||
with Image.open(path):
|
with Image.open(path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert str(e.value) == "Reached EOF while reading header"
|
|
||||||
|
|
||||||
# Test EOF for PyDecoder
|
# Test EOF for PyDecoder
|
||||||
fp = BytesIO(b"P5 3 1 4")
|
fp = BytesIO(b"P5 3 1 4")
|
||||||
with Image.open(fp) as im:
|
with Image.open(fp) as im:
|
||||||
|
@ -335,12 +331,12 @@ def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(b"P6\n3 1 " + maxval)
|
f.write(b"P6\n3 1 " + maxval)
|
||||||
|
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(
|
||||||
|
ValueError, match="maxval must be greater than 0 and less than 65536"
|
||||||
|
):
|
||||||
with Image.open(path):
|
with Image.open(path):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
|
|
||||||
|
|
||||||
|
|
||||||
def test_neg_ppm() -> None:
|
def test_neg_ppm() -> None:
|
||||||
# Storage.c accepted negative values for xsize, ysize. the
|
# Storage.c accepted negative values for xsize, ysize. the
|
||||||
|
|
|
@ -134,9 +134,8 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_set_legacy_api(self) -> None:
|
def test_set_legacy_api(self) -> None:
|
||||||
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
ifd = TiffImagePlugin.ImageFileDirectory_v2()
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception, match="Not allowing setting of legacy api"):
|
||||||
ifd.legacy_api = False
|
ifd.legacy_api = False
|
||||||
assert str(e.value) == "Not allowing setting of legacy api"
|
|
||||||
|
|
||||||
def test_xyres_tiff(self) -> None:
|
def test_xyres_tiff(self) -> None:
|
||||||
filename = "Tests/images/pil168.tif"
|
filename = "Tests/images/pil168.tif"
|
||||||
|
@ -661,6 +660,18 @@ class TestFileTiff:
|
||||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2[278] == 256
|
assert im.tag_v2[278] == 256
|
||||||
|
|
||||||
|
im = hopper()
|
||||||
|
im2 = Image.new("L", (128, 128))
|
||||||
|
im2.encoderinfo = {"tiffinfo": {278: 256}}
|
||||||
|
im.save(outfile, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
|
with Image.open(outfile) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
assert im.tag_v2[278] == 128
|
||||||
|
|
||||||
|
im.seek(1)
|
||||||
|
assert im.tag_v2[278] == 256
|
||||||
|
|
||||||
def test_strip_raw(self) -> None:
|
def test_strip_raw(self) -> None:
|
||||||
infile = "Tests/images/tiff_strip_raw.tif"
|
infile = "Tests/images/tiff_strip_raw.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
|
|
|
@ -154,9 +154,8 @@ class TestFileWebp:
|
||||||
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
||||||
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
|
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
|
||||||
im = Image.new("RGB", (15000, 15000))
|
im = Image.new("RGB", (15000, 15000))
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError, match="encoding error 6"):
|
||||||
im.save(tmp_path / "temp.webp", method=0)
|
im.save(tmp_path / "temp.webp", method=0)
|
||||||
assert str(e.value) == "encoding error 6"
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
||||||
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
|
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
|
||||||
|
@ -231,7 +230,7 @@ class TestFileWebp:
|
||||||
|
|
||||||
with Image.open(out_gif) as reread:
|
with Image.open(out_gif) as reread:
|
||||||
reread_value = reread.convert("RGB").getpixel((1, 1))
|
reread_value = reread.convert("RGB").getpixel((1, 1))
|
||||||
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
|
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3))
|
||||||
assert difference < 5
|
assert difference < 5
|
||||||
|
|
||||||
def test_duration(self, tmp_path: Path) -> None:
|
def test_duration(self, tmp_path: Path) -> None:
|
||||||
|
|
|
@ -65,9 +65,8 @@ class TestImage:
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
||||||
def test_image_modes_fail(self, mode: str) -> None:
|
def test_image_modes_fail(self, mode: str) -> None:
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError, match="unrecognized image mode"):
|
||||||
Image.new(mode, (1, 1))
|
Image.new(mode, (1, 1))
|
||||||
assert str(e.value) == "unrecognized image mode"
|
|
||||||
|
|
||||||
def test_exception_inheritance(self) -> None:
|
def test_exception_inheritance(self) -> None:
|
||||||
assert issubclass(UnidentifiedImageError, OSError)
|
assert issubclass(UnidentifiedImageError, OSError)
|
||||||
|
|
|
@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X
|
||||||
POINTS = (
|
POINTS = (
|
||||||
((10, 10), (20, 40), (30, 30)),
|
((10, 10), (20, 40), (30, 30)),
|
||||||
[(10, 10), (20, 40), (30, 30)],
|
[(10, 10), (20, 40), (30, 30)],
|
||||||
|
([10, 10], [20, 40], [30, 30]),
|
||||||
|
[[10, 10], [20, 40], [30, 30]],
|
||||||
(10, 10, 20, 40, 30, 30),
|
(10, 10, 20, 40, 30, 30),
|
||||||
[10, 10, 20, 40, 30, 30],
|
[10, 10, 20, 40, 30, 30],
|
||||||
)
|
)
|
||||||
|
@ -46,6 +48,8 @@ POINTS = (
|
||||||
KITE_POINTS = (
|
KITE_POINTS = (
|
||||||
((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
|
((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)),
|
||||||
[(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
|
[(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)],
|
||||||
|
([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]),
|
||||||
|
[[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1044,8 +1048,8 @@ def create_base_image_draw(
|
||||||
background2: tuple[int, int, int] = GRAY,
|
background2: tuple[int, int, int] = GRAY,
|
||||||
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
|
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
|
||||||
img = Image.new(mode, size, background1)
|
img = Image.new(mode, size, background1)
|
||||||
for x in range(0, size[0]):
|
for x in range(size[0]):
|
||||||
for y in range(0, size[1]):
|
for y in range(size[1]):
|
||||||
if (x + y) % 2 == 0:
|
if (x + y) % 2 == 0:
|
||||||
img.putpixel((x, y), background2)
|
img.putpixel((x, y), background2)
|
||||||
return img, ImageDraw.Draw(img)
|
return img, ImageDraw.Draw(img)
|
||||||
|
@ -1626,7 +1630,7 @@ def test_compute_regular_polygon_vertices(
|
||||||
0,
|
0,
|
||||||
ValueError,
|
ValueError,
|
||||||
"bounding_circle should contain 2D coordinates "
|
"bounding_circle should contain 2D coordinates "
|
||||||
"and a radius (e.g. (x, y, r) or ((x, y), r) )",
|
r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
3,
|
3,
|
||||||
|
@ -1640,7 +1644,7 @@ def test_compute_regular_polygon_vertices(
|
||||||
((50, 50, 50), 25),
|
((50, 50, 50), 25),
|
||||||
0,
|
0,
|
||||||
ValueError,
|
ValueError,
|
||||||
"bounding_circle centre should contain 2D coordinates (e.g. (x, y))",
|
r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
3,
|
3,
|
||||||
|
@ -1665,9 +1669,8 @@ def test_compute_regular_polygon_vertices_input_error_handling(
|
||||||
expected_error: type[Exception],
|
expected_error: type[Exception],
|
||||||
error_message: str,
|
error_message: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
with pytest.raises(expected_error) as e:
|
with pytest.raises(expected_error, match=error_message):
|
||||||
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
|
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
|
||||||
assert str(e.value) == error_message
|
|
||||||
|
|
||||||
|
|
||||||
def test_continuous_horizontal_edges_polygon() -> None:
|
def test_continuous_horizontal_edges_polygon() -> None:
|
||||||
|
|
|
@ -176,9 +176,8 @@ class TestImageFile:
|
||||||
b"0" * ImageFile.SAFEBLOCK
|
b"0" * ImageFile.SAFEBLOCK
|
||||||
) # only SAFEBLOCK bytes, so that the header is truncated
|
) # only SAFEBLOCK bytes, so that the header is truncated
|
||||||
)
|
)
|
||||||
with pytest.raises(OSError) as e:
|
with pytest.raises(OSError, match="Truncated File Read"):
|
||||||
BmpImagePlugin.BmpImageFile(b)
|
BmpImagePlugin.BmpImageFile(b)
|
||||||
assert str(e.value) == "Truncated File Read"
|
|
||||||
|
|
||||||
@skip_unless_feature("zlib")
|
@skip_unless_feature("zlib")
|
||||||
def test_truncated_with_errors(self) -> None:
|
def test_truncated_with_errors(self) -> None:
|
||||||
|
|
|
@ -80,15 +80,12 @@ def test_lut(op: str) -> None:
|
||||||
def test_no_operator_loaded() -> None:
|
def test_no_operator_loaded() -> None:
|
||||||
im = Image.new("L", (1, 1))
|
im = Image.new("L", (1, 1))
|
||||||
mop = ImageMorph.MorphOp()
|
mop = ImageMorph.MorphOp()
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception, match="No operator loaded"):
|
||||||
mop.apply(im)
|
mop.apply(im)
|
||||||
assert str(e.value) == "No operator loaded"
|
with pytest.raises(Exception, match="No operator loaded"):
|
||||||
with pytest.raises(Exception) as e:
|
|
||||||
mop.match(im)
|
mop.match(im)
|
||||||
assert str(e.value) == "No operator loaded"
|
with pytest.raises(Exception, match="No operator loaded"):
|
||||||
with pytest.raises(Exception) as e:
|
|
||||||
mop.save_lut("")
|
mop.save_lut("")
|
||||||
assert str(e.value) == "No operator loaded"
|
|
||||||
|
|
||||||
|
|
||||||
# Test the named patterns
|
# Test the named patterns
|
||||||
|
@ -238,15 +235,12 @@ def test_incorrect_mode() -> None:
|
||||||
im = hopper("RGB")
|
im = hopper("RGB")
|
||||||
mop = ImageMorph.MorphOp(op_name="erosion8")
|
mop = ImageMorph.MorphOp(op_name="erosion8")
|
||||||
|
|
||||||
with pytest.raises(ValueError) as e:
|
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||||
mop.apply(im)
|
mop.apply(im)
|
||||||
assert str(e.value) == "Image mode must be L"
|
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||||
with pytest.raises(ValueError) as e:
|
|
||||||
mop.match(im)
|
mop.match(im)
|
||||||
assert str(e.value) == "Image mode must be L"
|
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||||
with pytest.raises(ValueError) as e:
|
|
||||||
mop.get_on_pixels(im)
|
mop.get_on_pixels(im)
|
||||||
assert str(e.value) == "Image mode must be L"
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_patterns() -> None:
|
def test_add_patterns() -> None:
|
||||||
|
@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None:
|
||||||
lb.add_patterns(new_patterns)
|
lb.add_patterns(new_patterns)
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(
|
||||||
|
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
|
||||||
|
):
|
||||||
lb.build_lut()
|
lb.build_lut()
|
||||||
assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_invalid_mrl() -> None:
|
def test_load_invalid_mrl() -> None:
|
||||||
|
@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None:
|
||||||
mop = ImageMorph.MorphOp()
|
mop = ImageMorph.MorphOp()
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
with pytest.raises(Exception) as e:
|
with pytest.raises(Exception, match="Wrong size operator file!"):
|
||||||
mop.load_lut(invalid_mrl)
|
mop.load_lut(invalid_mrl)
|
||||||
assert str(e.value) == "Wrong size operator file!"
|
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip_mrl(tmp_path: Path) -> None:
|
def test_roundtrip_mrl(tmp_path: Path) -> None:
|
||||||
|
|
|
@ -112,7 +112,7 @@ def test_make_linear_lut() -> None:
|
||||||
assert isinstance(lut, list)
|
assert isinstance(lut, list)
|
||||||
assert len(lut) == 256
|
assert len(lut) == 256
|
||||||
# Check values
|
# Check values
|
||||||
for i in range(0, len(lut)):
|
for i in range(len(lut)):
|
||||||
assert lut[i] == i
|
assert lut[i] == i
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -68,25 +68,10 @@ def test_path_constructors(
|
||||||
assert list(p) == [(0.0, 1.0)]
|
assert list(p) == [(0.0, 1.0)]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
def test_invalid_path_constructors() -> None:
|
||||||
"coords",
|
# Arrange / Act
|
||||||
(
|
with pytest.raises(ValueError, match="incorrect coordinate type"):
|
||||||
("a", "b"),
|
ImagePath.Path(("a", "b"))
|
||||||
([0, 1],),
|
|
||||||
[[0, 1]],
|
|
||||||
([0.0, 1.0],),
|
|
||||||
[[0.0, 1.0]],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def test_invalid_path_constructors(
|
|
||||||
coords: tuple[str, str] | Sequence[Sequence[int]],
|
|
||||||
) -> None:
|
|
||||||
# Act
|
|
||||||
with pytest.raises(ValueError) as e:
|
|
||||||
ImagePath.Path(coords)
|
|
||||||
|
|
||||||
# Assert
|
|
||||||
assert str(e.value) == "incorrect coordinate type"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -99,13 +84,9 @@ def test_invalid_path_constructors(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
|
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
|
||||||
# Act
|
with pytest.raises(ValueError, match="wrong number of coordinates"):
|
||||||
with pytest.raises(ValueError) as e:
|
|
||||||
ImagePath.Path(coords)
|
ImagePath.Path(coords)
|
||||||
|
|
||||||
# Assert
|
|
||||||
assert str(e.value) == "wrong number of coordinates"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"coords, expected",
|
"coords, expected",
|
||||||
|
|
|
@ -32,7 +32,7 @@ def test_sanity(tmp_path: Path) -> None:
|
||||||
def test_iterator() -> None:
|
def test_iterator() -> None:
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
i = ImageSequence.Iterator(im)
|
i = ImageSequence.Iterator(im)
|
||||||
for index in range(0, im.n_frames):
|
for index in range(im.n_frames):
|
||||||
assert i[index] == next(i)
|
assert i[index] == next(i)
|
||||||
with pytest.raises(IndexError):
|
with pytest.raises(IndexError):
|
||||||
i[index + 1]
|
i[index + 1]
|
||||||
|
|
|
@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
|
||||||
("Tests/images/itxt_chunks.png", None),
|
("Tests/images/itxt_chunks.png", None),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
|
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
|
||||||
def test_pickle_image(
|
def test_pickle_image(
|
||||||
tmp_path: Path, test_file: str, test_mode: str | None, protocol: int
|
tmp_path: Path, test_file: str, test_mode: str | None, protocol: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -92,7 +92,7 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
|
||||||
im = im.convert("PA")
|
im = im.convert("PA")
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
|
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||||
im._mode = "LA"
|
im._mode = "LA"
|
||||||
with open(filename, "wb") as f:
|
with open(filename, "wb") as f:
|
||||||
pickle.dump(im, f, protocol)
|
pickle.dump(im, f, protocol)
|
||||||
|
@ -133,7 +133,7 @@ def helper_assert_pickled_font_images(
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("freetype2")
|
@skip_unless_feature("freetype2")
|
||||||
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
|
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
|
||||||
def test_pickle_font_string(protocol: int) -> None:
|
def test_pickle_font_string(protocol: int) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||||
|
@ -147,7 +147,7 @@ def test_pickle_font_string(protocol: int) -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("freetype2")
|
@skip_unless_feature("freetype2")
|
||||||
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
|
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
|
||||||
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
|
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||||
|
|
|
@ -68,7 +68,7 @@ by DirectX.
|
||||||
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
|
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
|
||||||
|
|
||||||
.. versionadded:: 3.4.0
|
.. versionadded:: 3.4.0
|
||||||
DXT3 images can be read in ``RGB`` mode and DX10 images can be read in
|
DXT3 images can be read in ``RGBA`` mode and DX10 images can be read in
|
||||||
``RGB`` and ``RGBA`` mode.
|
``RGB`` and ``RGBA`` mode.
|
||||||
|
|
||||||
.. versionadded:: 6.0.0
|
.. versionadded:: 6.0.0
|
||||||
|
@ -1163,9 +1163,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
||||||
|
|
||||||
**append_images**
|
**append_images**
|
||||||
A list of images to append as additional frames. Each of the
|
A list of images to append as additional frames. Each of the
|
||||||
images in the list can be single or multiframe images. Note however, that for
|
images in the list can be single or multiframe images.
|
||||||
correct results, all the appended images should have the same
|
|
||||||
``encoderinfo`` and ``encoderconfig`` properties.
|
|
||||||
|
|
||||||
.. versionadded:: 4.2.0
|
.. versionadded:: 4.2.0
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libtiff** provides compressed TIFF functionality
|
* **libtiff** provides compressed TIFF functionality
|
||||||
|
|
||||||
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
|
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.7.0**
|
||||||
|
|
||||||
* **libfreetype** provides type related services
|
* **libfreetype** provides type related services
|
||||||
|
|
||||||
|
|
|
@ -121,6 +121,7 @@ lint.select = [
|
||||||
"ISC", # flake8-implicit-str-concat
|
"ISC", # flake8-implicit-str-concat
|
||||||
"LOG", # flake8-logging
|
"LOG", # flake8-logging
|
||||||
"PGH", # pygrep-hooks
|
"PGH", # pygrep-hooks
|
||||||
|
"PIE", # flake8-pie
|
||||||
"PT", # flake8-pytest-style
|
"PT", # flake8-pytest-style
|
||||||
"PYI", # flake8-pyi
|
"PYI", # flake8-pyi
|
||||||
"RUF100", # unused noqa (yesqa)
|
"RUF100", # unused noqa (yesqa)
|
||||||
|
@ -133,6 +134,7 @@ lint.ignore = [
|
||||||
"E221", # Multiple spaces before operator
|
"E221", # Multiple spaces before operator
|
||||||
"E226", # Missing whitespace around arithmetic operator
|
"E226", # Missing whitespace around arithmetic operator
|
||||||
"E241", # Multiple spaces after ','
|
"E241", # Multiple spaces after ','
|
||||||
|
"PIE790", # flake8-pie: unnecessary-placeholder
|
||||||
"PT001", # pytest-fixture-incorrect-parentheses-style
|
"PT001", # pytest-fixture-incorrect-parentheses-style
|
||||||
"PT007", # pytest-parametrize-values-wrong-type
|
"PT007", # pytest-parametrize-values-wrong-type
|
||||||
"PT011", # pytest-raises-too-broad
|
"PT011", # pytest-raises-too-broad
|
||||||
|
|
|
@ -291,7 +291,7 @@ class BlpImageFile(ImageFile.ImageFile):
|
||||||
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
|
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
|
||||||
|
|
||||||
|
|
||||||
class _BLPBaseDecoder(ImageFile.PyDecoder):
|
class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
|
||||||
_pulls_fd = True
|
_pulls_fd = True
|
||||||
|
|
||||||
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
|
||||||
|
|
|
@ -123,8 +123,7 @@ def read_png_or_jpeg2000(
|
||||||
Image._decompression_bomb_check(im.size)
|
Image._decompression_bomb_check(im.size)
|
||||||
return {"RGBA": im}
|
return {"RGBA": im}
|
||||||
elif (
|
elif (
|
||||||
sig.startswith(b"\xff\x4f\xff\x51")
|
sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a"))
|
||||||
or sig.startswith(b"\x0d\x0a\x87\x0a")
|
|
||||||
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
|
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
|
||||||
):
|
):
|
||||||
if not enable_jpeg2k:
|
if not enable_jpeg2k:
|
||||||
|
|
|
@ -1005,7 +1005,7 @@ class Image:
|
||||||
elif len(mode) == 3:
|
elif len(mode) == 3:
|
||||||
transparency = tuple(
|
transparency = tuple(
|
||||||
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
|
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
|
||||||
for i in range(0, len(transparency))
|
for i in range(len(transparency))
|
||||||
)
|
)
|
||||||
new_im.info["transparency"] = transparency
|
new_im.info["transparency"] = transparency
|
||||||
return new_im
|
return new_im
|
||||||
|
@ -2479,7 +2479,21 @@ class Image:
|
||||||
format to use is determined from the filename extension.
|
format to use is determined from the filename extension.
|
||||||
If a file object was used instead of a filename, this
|
If a file object was used instead of a filename, this
|
||||||
parameter should always be used.
|
parameter should always be used.
|
||||||
:param params: Extra parameters to the image writer.
|
:param params: Extra parameters to the image writer. These can also be
|
||||||
|
set on the image itself through ``encoderinfo``. This is useful when
|
||||||
|
saving multiple images::
|
||||||
|
|
||||||
|
# Saving XMP data to a single image
|
||||||
|
from PIL import Image
|
||||||
|
red = Image.new("RGB", (1, 1), "#f00")
|
||||||
|
red.save("out.mpo", xmp=b"test")
|
||||||
|
|
||||||
|
# Saving XMP data to the second frame of an image
|
||||||
|
from PIL import Image
|
||||||
|
black = Image.new("RGB", (1, 1))
|
||||||
|
red = Image.new("RGB", (1, 1), "#f00")
|
||||||
|
red.encoderinfo = {"xmp": b"test"}
|
||||||
|
black.save("out.mpo", save_all=True, append_images=[red])
|
||||||
:returns: None
|
:returns: None
|
||||||
:exception ValueError: If the output format could not be determined
|
:exception ValueError: If the output format could not be determined
|
||||||
from the file name. Use the format option to solve this.
|
from the file name. Use the format option to solve this.
|
||||||
|
@ -2970,7 +2984,7 @@ class Image:
|
||||||
# Abstract handlers.
|
# Abstract handlers.
|
||||||
|
|
||||||
|
|
||||||
class ImagePointHandler:
|
class ImagePointHandler(abc.ABC):
|
||||||
"""
|
"""
|
||||||
Used as a mixin by point transforms
|
Used as a mixin by point transforms
|
||||||
(for use with :py:meth:`~PIL.Image.Image.point`)
|
(for use with :py:meth:`~PIL.Image.Image.point`)
|
||||||
|
@ -2981,7 +2995,7 @@ class ImagePointHandler:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ImageTransformHandler:
|
class ImageTransformHandler(abc.ABC):
|
||||||
"""
|
"""
|
||||||
Used as a mixin by geometry transforms
|
Used as a mixin by geometry transforms
|
||||||
(for use with :py:meth:`~PIL.Image.Image.transform`)
|
(for use with :py:meth:`~PIL.Image.Image.transform`)
|
||||||
|
@ -4007,7 +4021,7 @@ class Exif(_ExifBase):
|
||||||
ifd_data = tag_data[ifd_offset:]
|
ifd_data = tag_data[ifd_offset:]
|
||||||
|
|
||||||
makernote = {}
|
makernote = {}
|
||||||
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]):
|
for i in range(struct.unpack("<H", ifd_data[:2])[0]):
|
||||||
ifd_tag, typ, count, data = struct.unpack(
|
ifd_tag, typ, count, data = struct.unpack(
|
||||||
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
|
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2]
|
||||||
)
|
)
|
||||||
|
@ -4042,7 +4056,7 @@ class Exif(_ExifBase):
|
||||||
self._ifds[tag] = dict(self._fixup_dict(makernote))
|
self._ifds[tag] = dict(self._fixup_dict(makernote))
|
||||||
elif self.get(0x010F) == "Nintendo":
|
elif self.get(0x010F) == "Nintendo":
|
||||||
makernote = {}
|
makernote = {}
|
||||||
for i in range(0, struct.unpack(">H", tag_data[:2])[0]):
|
for i in range(struct.unpack(">H", tag_data[:2])[0]):
|
||||||
ifd_tag, typ, count, data = struct.unpack(
|
ifd_tag, typ, count, data = struct.unpack(
|
||||||
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
|
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1204,7 +1204,7 @@ def _compute_regular_polygon_vertices(
|
||||||
degrees = 360 / n_sides
|
degrees = 360 / n_sides
|
||||||
# Start with the bottom left polygon vertex
|
# Start with the bottom left polygon vertex
|
||||||
current_angle = (270 - 0.5 * degrees) + rotation
|
current_angle = (270 - 0.5 * degrees) + rotation
|
||||||
for _ in range(0, n_sides):
|
for _ in range(n_sides):
|
||||||
angles.append(current_angle)
|
angles.append(current_angle)
|
||||||
current_angle += degrees
|
current_angle += degrees
|
||||||
if current_angle > 360:
|
if current_angle > 360:
|
||||||
|
@ -1227,4 +1227,4 @@ def _color_diff(
|
||||||
first = color1 if isinstance(color1, tuple) else (color1,)
|
first = color1 if isinstance(color1, tuple) else (color1,)
|
||||||
second = color2 if isinstance(color2, tuple) else (color2,)
|
second = color2 if isinstance(color2, tuple) else (color2,)
|
||||||
|
|
||||||
return sum(abs(first[i] - second[i]) for i in range(0, len(second)))
|
return sum(abs(first[i] - second[i]) for i in range(len(second)))
|
||||||
|
|
|
@ -438,7 +438,7 @@ class ImageFile(Image.Image):
|
||||||
return self.tell() != frame
|
return self.tell() != frame
|
||||||
|
|
||||||
|
|
||||||
class StubHandler:
|
class StubHandler(abc.ABC):
|
||||||
def open(self, im: StubImageFile) -> None:
|
def open(self, im: StubImageFile) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -447,7 +447,7 @@ class StubHandler:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class StubImageFile(ImageFile):
|
class StubImageFile(ImageFile, metaclass=abc.ABCMeta):
|
||||||
"""
|
"""
|
||||||
Base class for stub image loaders.
|
Base class for stub image loaders.
|
||||||
|
|
||||||
|
@ -455,9 +455,9 @@ class StubImageFile(ImageFile):
|
||||||
certain format, but relies on external code to load the file.
|
certain format, but relies on external code to load the file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
def _open(self) -> None:
|
def _open(self) -> None:
|
||||||
msg = "StubImageFile subclass must implement _open"
|
pass
|
||||||
raise NotImplementedError(msg)
|
|
||||||
|
|
||||||
def load(self) -> Image.core.PixelAccess | None:
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
loader = self._load()
|
loader = self._load()
|
||||||
|
@ -471,10 +471,10 @@ class StubImageFile(ImageFile):
|
||||||
self.__dict__ = image.__dict__
|
self.__dict__ = image.__dict__
|
||||||
return image.load()
|
return image.load()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
def _load(self) -> StubHandler | None:
|
def _load(self) -> StubHandler | None:
|
||||||
"""(Hook) Find actual image loader."""
|
"""(Hook) Find actual image loader."""
|
||||||
msg = "StubImageFile subclass must implement _load"
|
pass
|
||||||
raise NotImplementedError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
|
|
|
@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
||||||
from ._typing import NumpyArray
|
from ._typing import NumpyArray
|
||||||
|
|
||||||
|
|
||||||
class Filter:
|
class Filter(abc.ABC):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -213,14 +213,14 @@ def colorize(
|
||||||
blue = []
|
blue = []
|
||||||
|
|
||||||
# Create the low-end values
|
# Create the low-end values
|
||||||
for i in range(0, blackpoint):
|
for i in range(blackpoint):
|
||||||
red.append(rgb_black[0])
|
red.append(rgb_black[0])
|
||||||
green.append(rgb_black[1])
|
green.append(rgb_black[1])
|
||||||
blue.append(rgb_black[2])
|
blue.append(rgb_black[2])
|
||||||
|
|
||||||
# Create the mapping (2-color)
|
# Create the mapping (2-color)
|
||||||
if rgb_mid is None:
|
if rgb_mid is None:
|
||||||
range_map = range(0, whitepoint - blackpoint)
|
range_map = range(whitepoint - blackpoint)
|
||||||
|
|
||||||
for i in range_map:
|
for i in range_map:
|
||||||
red.append(
|
red.append(
|
||||||
|
@ -235,8 +235,8 @@ def colorize(
|
||||||
|
|
||||||
# Create the mapping (3-color)
|
# Create the mapping (3-color)
|
||||||
else:
|
else:
|
||||||
range_map1 = range(0, midpoint - blackpoint)
|
range_map1 = range(midpoint - blackpoint)
|
||||||
range_map2 = range(0, whitepoint - midpoint)
|
range_map2 = range(whitepoint - midpoint)
|
||||||
|
|
||||||
for i in range_map1:
|
for i in range_map1:
|
||||||
red.append(
|
red.append(
|
||||||
|
@ -256,7 +256,7 @@ def colorize(
|
||||||
blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
|
blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
|
||||||
|
|
||||||
# Create the high-end values
|
# Create the high-end values
|
||||||
for i in range(0, 256 - whitepoint):
|
for i in range(256 - whitepoint):
|
||||||
red.append(rgb_white[0])
|
red.append(rgb_white[0])
|
||||||
green.append(rgb_white[1])
|
green.append(rgb_white[1])
|
||||||
blue.append(rgb_white[2])
|
blue.append(rgb_white[2])
|
||||||
|
|
|
@ -192,7 +192,7 @@ if sys.platform == "darwin":
|
||||||
register(MacViewer)
|
register(MacViewer)
|
||||||
|
|
||||||
|
|
||||||
class UnixViewer(Viewer):
|
class UnixViewer(abc.ABC, Viewer):
|
||||||
format = "PNG"
|
format = "PNG"
|
||||||
options = {"compress_level": 1, "save_all": True}
|
options = {"compress_level": 1, "save_all": True}
|
||||||
|
|
||||||
|
|
|
@ -569,7 +569,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
|
||||||
mpentries = []
|
mpentries = []
|
||||||
try:
|
try:
|
||||||
rawmpentries = mp[0xB002]
|
rawmpentries = mp[0xB002]
|
||||||
for entrynum in range(0, quant):
|
for entrynum in range(quant):
|
||||||
unpackedentry = struct.unpack_from(
|
unpackedentry = struct.unpack_from(
|
||||||
f"{endianness}LLLHH", rawmpentries, entrynum * 16
|
f"{endianness}LLLHH", rawmpentries, entrynum * 16
|
||||||
)
|
)
|
||||||
|
|
|
@ -1584,7 +1584,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
# byte order.
|
# byte order.
|
||||||
elif rawmode == "I;16":
|
elif rawmode == "I;16":
|
||||||
rawmode = "I;16N"
|
rawmode = "I;16N"
|
||||||
elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
|
elif rawmode.endswith((";16B", ";16L")):
|
||||||
rawmode = rawmode[:-1] + "N"
|
rawmode = rawmode[:-1] + "N"
|
||||||
|
|
||||||
# Offset in the tile tuple is 0, we go from 0,0 to
|
# Offset in the tile tuple is 0, we go from 0,0 to
|
||||||
|
@ -2295,9 +2295,7 @@ class AppendingTiffWriter(io.BytesIO):
|
||||||
|
|
||||||
|
|
||||||
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
encoderinfo = im.encoderinfo.copy()
|
append_images = list(im.encoderinfo.get("append_images", []))
|
||||||
encoderconfig = im.encoderconfig
|
|
||||||
append_images = list(encoderinfo.get("append_images", []))
|
|
||||||
if not hasattr(im, "n_frames") and not append_images:
|
if not hasattr(im, "n_frames") and not append_images:
|
||||||
return _save(im, fp, filename)
|
return _save(im, fp, filename)
|
||||||
|
|
||||||
|
@ -2305,12 +2303,11 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
try:
|
try:
|
||||||
with AppendingTiffWriter(fp) as tf:
|
with AppendingTiffWriter(fp) as tf:
|
||||||
for ims in [im] + append_images:
|
for ims in [im] + append_images:
|
||||||
ims.encoderinfo = encoderinfo
|
if not hasattr(ims, "encoderinfo"):
|
||||||
ims.encoderconfig = encoderconfig
|
ims.encoderinfo = {}
|
||||||
if not hasattr(ims, "n_frames"):
|
if not hasattr(ims, "encoderconfig"):
|
||||||
nfr = 1
|
ims.encoderconfig = ()
|
||||||
else:
|
nfr = getattr(ims, "n_frames", 1)
|
||||||
nfr = ims.n_frames
|
|
||||||
|
|
||||||
for idx in range(nfr):
|
for idx in range(nfr):
|
||||||
ims.seek(idx)
|
ims.seek(idx)
|
||||||
|
|
|
@ -615,6 +615,7 @@ static const struct j2k_decode_unpacker j2k_unpackers[] = {
|
||||||
{"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la},
|
{"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la},
|
||||||
{"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb},
|
{"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb},
|
||||||
{"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb},
|
{"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb},
|
||||||
|
{"RGBA", OPJ_CLRSPC_GRAY, 4, 1, j2ku_srgba_rgba},
|
||||||
{"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba},
|
{"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba},
|
||||||
{"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba},
|
{"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba},
|
||||||
{"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba},
|
{"CMYK", OPJ_CLRSPC_CMYK, 4, 1, j2ku_srgba_rgba},
|
||||||
|
|
78
src/path.c
78
src/path.c
|
@ -109,6 +109,39 @@ path_dealloc(PyPathObject *path) {
|
||||||
|
|
||||||
#define PyPath_Check(op) (Py_TYPE(op) == &PyPathType)
|
#define PyPath_Check(op) (Py_TYPE(op) == &PyPathType)
|
||||||
|
|
||||||
|
static int
|
||||||
|
assign_item_to_array(double *xy, Py_ssize_t j, PyObject *op) {
|
||||||
|
if (PyFloat_Check(op)) {
|
||||||
|
xy[j++] = PyFloat_AS_DOUBLE(op);
|
||||||
|
} else if (PyLong_Check(op)) {
|
||||||
|
xy[j++] = (float)PyLong_AS_LONG(op);
|
||||||
|
} else if (PyNumber_Check(op)) {
|
||||||
|
xy[j++] = PyFloat_AsDouble(op);
|
||||||
|
} else if (PyList_Check(op)) {
|
||||||
|
for (int k = 0; k < 2; k++) {
|
||||||
|
PyObject *op1 = PyList_GetItemRef(op, k);
|
||||||
|
if (op1 == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
j = assign_item_to_array(xy, j, op1);
|
||||||
|
Py_DECREF(op1);
|
||||||
|
if (j == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
double x, y;
|
||||||
|
if (PyArg_ParseTuple(op, "dd", &x, &y)) {
|
||||||
|
xy[j++] = x;
|
||||||
|
xy[j++] = y;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "incorrect coordinate type");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
Py_ssize_t
|
Py_ssize_t
|
||||||
PyPath_Flatten(PyObject *data, double **pxy) {
|
PyPath_Flatten(PyObject *data, double **pxy) {
|
||||||
Py_ssize_t i, j, n;
|
Py_ssize_t i, j, n;
|
||||||
|
@ -164,48 +197,32 @@ PyPath_Flatten(PyObject *data, double **pxy) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define assign_item_to_array(op, decref) \
|
|
||||||
if (PyFloat_Check(op)) { \
|
|
||||||
xy[j++] = PyFloat_AS_DOUBLE(op); \
|
|
||||||
} else if (PyLong_Check(op)) { \
|
|
||||||
xy[j++] = (float)PyLong_AS_LONG(op); \
|
|
||||||
} else if (PyNumber_Check(op)) { \
|
|
||||||
xy[j++] = PyFloat_AsDouble(op); \
|
|
||||||
} else if (PyArg_ParseTuple(op, "dd", &x, &y)) { \
|
|
||||||
xy[j++] = x; \
|
|
||||||
xy[j++] = y; \
|
|
||||||
} else { \
|
|
||||||
PyErr_SetString(PyExc_ValueError, "incorrect coordinate type"); \
|
|
||||||
if (decref) { \
|
|
||||||
Py_DECREF(op); \
|
|
||||||
} \
|
|
||||||
free(xy); \
|
|
||||||
return -1; \
|
|
||||||
} \
|
|
||||||
if (decref) { \
|
|
||||||
Py_DECREF(op); \
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Copy table to path array */
|
/* Copy table to path array */
|
||||||
if (PyList_Check(data)) {
|
if (PyList_Check(data)) {
|
||||||
for (i = 0; i < n; i++) {
|
for (i = 0; i < n; i++) {
|
||||||
double x, y;
|
|
||||||
PyObject *op = PyList_GetItemRef(data, i);
|
PyObject *op = PyList_GetItemRef(data, i);
|
||||||
if (op == NULL) {
|
if (op == NULL) {
|
||||||
free(xy);
|
free(xy);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
assign_item_to_array(op, 1);
|
j = assign_item_to_array(xy, j, op);
|
||||||
|
Py_DECREF(op);
|
||||||
|
if (j == -1) {
|
||||||
|
free(xy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (PyTuple_Check(data)) {
|
} else if (PyTuple_Check(data)) {
|
||||||
for (i = 0; i < n; i++) {
|
for (i = 0; i < n; i++) {
|
||||||
double x, y;
|
|
||||||
PyObject *op = PyTuple_GET_ITEM(data, i);
|
PyObject *op = PyTuple_GET_ITEM(data, i);
|
||||||
assign_item_to_array(op, 0);
|
j = assign_item_to_array(xy, j, op);
|
||||||
|
if (j == -1) {
|
||||||
|
free(xy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (i = 0; i < n; i++) {
|
for (i = 0; i < n; i++) {
|
||||||
double x, y;
|
|
||||||
PyObject *op = PySequence_GetItem(data, i);
|
PyObject *op = PySequence_GetItem(data, i);
|
||||||
if (!op) {
|
if (!op) {
|
||||||
/* treat IndexError as end of sequence */
|
/* treat IndexError as end of sequence */
|
||||||
|
@ -217,7 +234,12 @@ PyPath_Flatten(PyObject *data, double **pxy) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assign_item_to_array(op, 1);
|
j = assign_item_to_array(xy, j, op);
|
||||||
|
Py_DECREF(op);
|
||||||
|
if (j == -1) {
|
||||||
|
free(xy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ V = {
|
||||||
"LIBPNG": "1.6.47",
|
"LIBPNG": "1.6.47",
|
||||||
"LIBWEBP": "1.5.0",
|
"LIBWEBP": "1.5.0",
|
||||||
"OPENJPEG": "2.5.3",
|
"OPENJPEG": "2.5.3",
|
||||||
"TIFF": "4.6.0",
|
"TIFF": "4.7.0",
|
||||||
"XZ": "5.6.4",
|
"XZ": "5.6.4",
|
||||||
"ZLIBNG": "2.2.4",
|
"ZLIBNG": "2.2.4",
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user