Merge branch 'main' into progress

This commit is contained in:
Andrew Murray 2024-07-16 20:05:10 +10:00 committed by GitHub
commit 8b4b7ce7dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 2359 additions and 512 deletions

View File

@ -37,12 +37,18 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy
# TODO Update condition when NumPy supports free-threading
if [[ "$PYTHON_GIL" == "0" ]]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6
# TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
fi
# Pyroma uses non-isolated build and fails with old setuptools

View File

@ -50,26 +50,24 @@ jobs:
"3.9",
]
include:
- python-version: "3.11"
PYTHONOPTIMIZE: 1
REVERSE: "--reverse"
- python-version: "3.10"
PYTHONOPTIMIZE: 2
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
# M1 only available for 3.10+
- os: "macos-13"
python-version: "3.9"
- { os: "macos-13", python-version: "3.9" }
exclude:
- os: "macos-14"
python-version: "3.9"
- { os: "macos-14", python-version: "3.9" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
if: "${{ !matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@ -78,6 +76,18 @@ jobs:
".ci/*.sh"
"pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
nogil: ${{ matrix.disable-gil }}
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information
run: python3 .github/workflows/system-info.py

View File

@ -12,8 +12,14 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
# TODO Update condition when NumPy supports free-threading
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
fi
if [ ! -d "test-images-main" ]; then

View File

@ -41,11 +41,8 @@ jobs:
python-version:
- pp39
- pp310
- cp39
- cp310
- cp311
- cp312
- cp313
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
@ -132,6 +129,7 @@ jobs:
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_FREE_THREADED_SUPPORT: True
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
@ -204,6 +202,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_FREE_THREADED_SUPPORT: True
CIBW_PRERELEASE_PYTHONS: True
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm

View File

@ -2,6 +2,21 @@
Changelog (Pillow)
==================
11.0.0 (unreleased)
-------------------
- Drop support for Python 3.8 #8183
[hugovk, radarhere]
- Add support for Python 3.13 #8181
[hugovk, radarhere]
- Fix incompatibility with NumPy 1.20 #8187
[neutrinoceros, radarhere]
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
[hugovk, radarhere]
10.4.0 (2024-07-01)
-------------------

View File

@ -60,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b
def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:

View File

@ -401,7 +401,7 @@ def test_palette_434(tmp_path: Path) -> None:
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
im.copy().save(out, **kwargs)
im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)
return reloaded

View File

@ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f:
s.write(f.read())
s.seek(0)
r = io.BufferedReader(s)
data = f.read()
class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result
def readable(self) -> bool:
return True
r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im:
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
@ -1048,7 +1059,11 @@ class TestFileLibTiff(LibTiffTestCase):
],
)
def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile
self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
@ -1135,7 +1150,7 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import warnings
from io import BytesIO
from typing import Any, cast
from typing import Any
import pytest
from PIL import Image, MpoImagePlugin
from PIL import Image, ImageFile, MpoImagePlugin
from .helper import (
assert_image_equal,
@ -20,11 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg")
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "MPO", **options)
out.seek(0)
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
return Image.open(out)
@pytest.mark.parametrize("test_file", test_files)
@ -226,6 +226,12 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)
def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
@ -275,6 +281,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)

View File

@ -118,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params)
im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
contents = fp.read()
@ -271,6 +271,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
assert pdf.pages_ref is not None
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info

View File

@ -41,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC
def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data)
PngImagePlugin.putchunk(test_file, cid, *data)
return test_file.getvalue()

View File

@ -78,6 +78,7 @@ class TestFileTiff:
def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff")
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close()
with pytest.raises(ValueError):
@ -424,13 +425,13 @@ class TestFileTiff:
def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd"
ret = ifd.load_float(data, False)
ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)
def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False)
ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194)
def test_ifd_tag_type(self) -> None:
@ -599,7 +600,7 @@ class TestFileTiff:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs)
hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
@ -624,14 +625,17 @@ class TestFileTiff:
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif")
im = hopper()
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)
with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 33723 not in im.tag_v2
def test_rowsperstrip(self, tmp_path: Path) -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Generator
from pathlib import Path
import pytest
@ -96,7 +97,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
check(temp_file1)
# Tests appending using a generator
def im_generator(ims):
def im_generator(
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims
temp_file2 = str(tmp_path / "temp_generator.webp")

View File

@ -372,8 +372,9 @@ class TestImage:
img = Image.alpha_composite(dst, src)
# Assert
img_colors = sorted(img.getcolors())
assert img_colors == expected_colors
img_colors = img.getcolors()
assert img_colors is not None
assert sorted(img_colors) == expected_colors
def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
@ -670,7 +671,9 @@ class TestImage:
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6
palette = im_remapped.getpalette()
assert palette is not None
assert len(palette) == 6
# Test unused transparency
im.info["transparency"] = 2
@ -701,7 +704,7 @@ class TestImage:
else:
assert new_image.palette is None
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
_make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())

View File

@ -27,7 +27,9 @@ class TestImagePutPixel:
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@ -37,7 +39,9 @@ class TestImagePutPixel:
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@ -50,9 +54,9 @@ class TestImagePutPixel:
assert pix1 is not None
assert pix2 is not None
with pytest.raises(TypeError):
pix1[0, "0"]
pix1[0, "0"] # type: ignore[index]
with pytest.raises(TypeError):
pix1["0", 0]
pix1["0", 0] # type: ignore[index]
for y in range(im1.size[1]):
for x in range(im1.size[0]):
@ -71,7 +75,9 @@ class TestImagePutPixel:
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert_image_equal(im1, im2)
@ -81,7 +87,9 @@ class TestImagePutPixel:
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pos = x, y
im2.putpixel(pos, im1.getpixel(pos))
value = im1.getpixel(pos)
assert value is not None
im2.putpixel(pos, value)
assert not im2.readonly
assert_image_equal(im1, im2)
@ -219,7 +227,7 @@ class TestImagePutPixelError:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"):
im.putpixel((0, 0), v)
im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize(
("mode", "band_numbers", "match"),
@ -253,7 +261,7 @@ class TestImagePutPixelError:
with pytest.raises(
TypeError, match="color must be int or single-element tuple"
):
im.putpixel((0, 0), v)
im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode: str) -> None:

View File

@ -225,7 +225,7 @@ def test_l_macro_rounding(convert_mode: str) -> None:
assert px is not None
converted_color = px[0, 0]
if convert_mode == "LA":
assert converted_color is not None
assert isinstance(converted_color, tuple)
converted_color = converted_color[0]
assert converted_color == 1

View File

@ -54,17 +54,21 @@ def test_pack() -> None:
assert A is None
A = im.getcolors(maxcolors=3)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=4)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=8)
assert A is not None
A.sort()
assert A == expected
A = im.getcolors(maxcolors=16)
assert A is not None
A.sort()
assert A == expected

View File

@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None:
# see bug-200802-systemerror
def put(value: int) -> tuple[int, int, int, int]:
def put(value: int) -> float | tuple[int, ...] | None:
im = Image.new("RGBA", (1, 1))
im.putdata([value])
return im.getpixel((0, 0))

View File

@ -31,7 +31,9 @@ def test_libimagequant_quantize() -> None:
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_octree_quantize() -> None:
@ -39,7 +41,9 @@ def test_octree_quantize() -> None:
converted = image.quantize(100, Image.Quantize.FASTOCTREE)
assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100
colors = converted.getcolors()
assert colors is not None
assert len(colors) == 100
def test_rgba_quantize() -> None:
@ -158,4 +162,6 @@ def test_small_palette() -> None:
im = im.quantize(palette=p)
# Assert
assert len(im.getcolors()) == 2
quantized_colors = im.getcolors()
assert quantized_colors is not None
assert len(quantized_colors) == 2

View File

@ -237,13 +237,13 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency:
def make_case(
self, mode: str, fill: tuple[int, int, int] | float
) -> tuple[Image.Image, tuple[int, ...]]:
) -> tuple[Image.Image, float | tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill)
px = im.load()
assert px is not None
return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None:
channel, color = case
px = channel.load()
assert px is not None
@ -256,6 +256,7 @@ class TestCoreResampleConsistency:
def test_8u(self) -> None:
im, color = self.make_case("RGB", (0, 64, 255))
r, g, b = im.split()
assert isinstance(color, tuple)
self.run_case((r, color[0]))
self.run_case((g, color[1]))
self.run_case((b, color[2]))
@ -290,7 +291,11 @@ class TestCoreResampleAlphaCorrect:
px = i.load()
assert px is not None
for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])}
used_colors = set()
for x in range(i.size[0]):
value = px[x, y]
assert isinstance(value, tuple)
used_colors.add(value[0])
assert 256 == len(used_colors), (
"All colors should be present in resized image. "
f"Only {len(used_colors)} on line {y}."
@ -332,12 +337,13 @@ class TestCoreResampleAlphaCorrect:
assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel:
value = px[x, y]
assert isinstance(value, tuple)
if value[-1] != 0 and value[:-1] != clean_pixel:
message = (
f"pixel at ({x}, {y}) is different:\n"
f"{px[x, y]}\n{clean_pixel}"
f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}"
)
assert px[x, y][:3] == clean_pixel, message
assert value[:3] == clean_pixel, message
def test_dirty_pixels_rgba(self) -> None:
case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0))

View File

@ -285,14 +285,14 @@ class TestReducingGapResize:
class TestImageResize:
def test_resize(self) -> None:
def resize(mode: str, size: tuple[int, int]) -> None:
def resize(mode: str, size: tuple[int, int] | list[int]) -> None:
out = hopper(mode).resize(size)
assert out.mode == mode
assert out.size == size
assert out.size == tuple(size)
for mode in "1", "P", "L", "RGB", "I", "F":
resize(mode, (112, 103))
resize(mode, (188, 214))
resize(mode, [188, 214])
# Test unknown resampling filter
with hopper() as im:

View File

@ -192,8 +192,9 @@ class TestImageTransform:
im = op(im, (40, 10))
colors = sorted(im.getcolors())
assert colors == sorted(
colors = im.getcolors()
assert colors is not None
assert sorted(colors) == sorted(
(
(20 * 10, opaque),
(20 * 10, transparent),

View File

@ -391,23 +391,25 @@ def test_overlay() -> None:
def test_logical() -> None:
def table(
op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int
) -> tuple[int, int, int, int]:
) -> list[float]:
out = []
for x in (a, b):
imx = Image.new("1", (1, 1), x)
for y in (a, b):
imy = Image.new("1", (1, 1), y)
out.append(op(imx, imy).getpixel((0, 0)))
return tuple(out)
value = op(imx, imy).getpixel((0, 0))
assert not isinstance(value, tuple) and value is not None
out.append(value)
return out
assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0]
assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255)
assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255)
assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0)
assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255]
assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255]
assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0]

View File

@ -691,7 +691,9 @@ def test_rgb_lab(mode: str) -> None:
im = Image.new("LAB", (1, 1), (255, 0, 0))
converted_im = im.convert(mode)
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
value = converted_im.getpixel((0, 0))
assert isinstance(value, tuple)
assert value[:3] == (0, 255, 255)
def test_deprecation() -> None:

View File

@ -1,8 +1,8 @@
from __future__ import annotations
import contextlib
import os.path
from collections.abc import Sequence
from typing import Callable
import pytest
@ -1422,25 +1422,44 @@ def test_default_font_size() -> None:
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def check(func: Callable[[], None]) -> None:
if freetype_support:
func()
else:
with pytest.raises(ImportError):
func()
def draw_text() -> None:
draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_text)
def draw_textlength() -> None:
assert draw.textlength(text, font_size=16) == 216
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_textlength)
def draw_textbbox() -> None:
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_textbbox)
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
def draw_multiline_text() -> None:
draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
check(draw_multiline_text)
def draw_multiline_textbbox() -> None:
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
check(draw_multiline_textbbox)
@pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox: Coords) -> None:

View File

@ -90,6 +90,7 @@ class TestImageFile:
data = f.read()
with ImageFile.Parser() as p:
p.feed(data)
assert p.image is not None
assert (48, 48) == p.image.size
@skip_unless_feature("webp")
@ -103,6 +104,7 @@ class TestImageFile:
assert not p.image
p.feed(f.read())
assert p.image is not None
assert (128, 128) == p.image.size
@skip_unless_feature("zlib")
@ -125,7 +127,7 @@ class TestImageFile:
def test_raise_typeerror(self) -> None:
with pytest.raises(TypeError):
parser = ImageFile.Parser()
parser.feed(1)
parser.feed(1) # type: ignore[arg-type]
def test_negative_stride(self) -> None:
with open("Tests/images/raw_negative_stride.bin", "rb") as f:
@ -303,9 +305,9 @@ class TestPyDecoder(CodecsTest):
im.load()
def test_decode(self) -> None:
decoder = ImageFile.PyDecoder(None)
decoder = ImageFile.PyDecoder("")
with pytest.raises(NotImplementedError):
decoder.decode(None)
decoder.decode(b"")
class TestPyEncoder(CodecsTest):
@ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest):
)
def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None)
encoder = ImageFile.PyEncoder("")
with pytest.raises(NotImplementedError):
encoder.encode(0)
@ -393,8 +395,9 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()
fh = BytesIO()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(None, None)
encoder.encode_to_file(fh, 0)
def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError):

View File

@ -60,6 +60,8 @@ class TestImageGrab:
def test_grabclipboard(self) -> None:
if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"])
ImageGrab.grabclipboard()
elif sys.platform == "win32":
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
p.stdin.write(
@ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200
[Windows.Forms.Clipboard]::SetImage($bmp)"""
)
p.communicate()
ImageGrab.grabclipboard()
else:
if not shutil.which("wl-paste") and not shutil.which("xclip"):
with pytest.raises(
@ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200
r" ImageGrab.grabclipboard\(\) on Linux",
):
ImageGrab.grabclipboard()
return
ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None:

View File

@ -41,11 +41,15 @@ A = string_to_img(
def img_to_string(im: Image.Image) -> str:
"""Turn a (small) binary image into a string representation"""
chars = ".1"
width, height = im.size
return "\n".join(
"".join(chars[im.getpixel((c, r)) > 0] for c in range(width))
for r in range(height)
)
result = []
for r in range(im.height):
line = ""
for c in range(im.width):
value = im.getpixel((c, r))
assert not isinstance(value, tuple) and value is not None
line += chars[value > 0]
result.append(line)
return "\n".join(result)
def img_string_normalize(im: str) -> str:

View File

@ -259,20 +259,26 @@ def test_colorize_2color() -> None:
left = (0, 1)
middle = (127, 1)
right = (255, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle),
value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -295,20 +301,26 @@ def test_colorize_2color_offset() -> None:
left = (25, 1)
middle = (75, 1)
right = (125, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle),
value,
(127, 63, 0),
threshold=1,
msg="mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -339,29 +351,37 @@ def test_colorize_3color_offset() -> None:
middle = (100, 1)
right_middle = (150, 1)
right = (225, 1)
value = im_test.getpixel(left)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left),
value,
(255, 0, 0),
threshold=1,
msg="black test pixel incorrect",
)
value = im_test.getpixel(left_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(left_middle),
value,
(127, 0, 127),
threshold=1,
msg="low-mid test pixel incorrect",
)
value = im_test.getpixel(middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
value = im_test.getpixel(right_middle)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect"
)
assert_tuple_approx_equal(
im_test.getpixel(right_middle),
value,
(0, 63, 127),
threshold=1,
msg="high-mid test pixel incorrect",
)
value = im_test.getpixel(right)
assert isinstance(value, tuple)
assert_tuple_approx_equal(
im_test.getpixel(right),
value,
(0, 127, 0),
threshold=1,
msg="white test pixel incorrect",
@ -444,6 +464,7 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()

View File

@ -16,8 +16,11 @@ def test_sanity() -> None:
def test_register() -> None:
# Test registering a viewer that is not a class
ImageShow.register("not a class")
# Test registering a viewer that is an instance
class TestViewer(ImageShow.Viewer):
pass
ImageShow.register(TestViewer())
# Restore original state
ImageShow._viewers.pop()

View File

@ -45,10 +45,12 @@ def test_kw() -> None:
# Test "file"
im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im1)
# Test "data"
im = ImageTk._get_image_from_kw(kw)
assert im is not None
assert_image_equal(im, im2)
# Test no relevant entry
@ -107,3 +109,6 @@ def test_bitmapimage() -> None:
# reloaded = ImageTk.getimage(im_tk)
# assert_image_equal(reloaded, im)
with pytest.raises(ValueError):
ImageTk.BitmapImage()

View File

@ -57,6 +57,9 @@ class TestImageWinDib:
# Assert
assert dib.size == (128, 128)
with pytest.raises(ValueError):
ImageWin.Dib(mode)
def test_dib_paste(self) -> None:
# Arrange
im = hopper()

View File

@ -198,6 +198,15 @@ def test_putdata() -> None:
assert len(im.getdata()) == len(arr)
def test_resize() -> None:
im = hopper()
size = (64, 64)
im_resized = im.resize(numpy.array(size))
assert im_resized.size == size
@pytest.mark.parametrize(
"dtype",
(

View File

@ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational
from .helper import hopper, skip_unless_feature
def _test_equal(num, denom, target) -> None:
def _test_equal(
num: float | Fraction | IFDRational,
denom: int,
target: float | Fraction | IFDRational,
) -> None:
t = IFDRational(num, denom)
assert target == t

View File

@ -75,7 +75,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0 |arm |
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0 |arm |
+----------------------------------+----------------------------+------------------+--------------+
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +----------------------------+------------------+ |

View File

@ -155,3 +155,11 @@ follow_imports = "silent"
warn_redundant_casts = true
warn_unreachable = true
warn_unused_ignores = true
exclude = [
'^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$',
'^Tests/test_qt_image_qapplication.py$',
'^Tests/test_font_pcf_charsets.py$',
'^Tests/test_font_pcf.py$',
'^Tests/test_file_tar.py$',
]

View File

@ -313,6 +313,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length: int) -> bytes:
assert self.fd is not None
return ImageFile._safe_read(self.fd, length)
def _read_palette(self) -> list[tuple[int, int, int, int]]:

View File

@ -25,7 +25,7 @@
from __future__ import annotations
import os
from typing import IO
from typing import IO, Any
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@ -72,16 +72,20 @@ class BmpImageFile(ImageFile.ImageFile):
for k, v in COMPRESSIONS.items():
vars()[k] = v
def _bitmap(self, header=0, offset=0):
def _bitmap(self, header: int = 0, offset: int = 0) -> None:
"""Read relevant info about the BMP"""
read, seek = self.fp.read, self.fp.seek
if header:
seek(header)
# read bmp header size @offset 14 (this is part of the header size)
file_info = {"header_size": i32(read(4)), "direction": -1}
file_info: dict[str, bool | int | tuple[int, ...]] = {
"header_size": i32(read(4)),
"direction": -1,
}
# -------------------- If requested, read header at a specific position
# read the rest of the bmp header, without its size
assert isinstance(file_info["header_size"], int)
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
@ -92,7 +96,7 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["height"] = i16(header_data, 2)
file_info["planes"] = i16(header_data, 4)
file_info["bits"] = i16(header_data, 6)
file_info["compression"] = self.RAW
file_info["compression"] = self.COMPRESSIONS["RAW"]
file_info["palette_padding"] = 3
# --------------------------------------------- Windows Bitmap v3 to v5
@ -122,8 +126,9 @@ class BmpImageFile(ImageFile.ImageFile):
)
file_info["colors"] = i32(header_data, 28)
file_info["palette_padding"] = 4
assert isinstance(file_info["pixels_per_meter"], tuple)
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
if file_info["compression"] == self.BITFIELDS:
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
masks = ["r_mask", "g_mask", "b_mask"]
if len(header_data) >= 48:
if len(header_data) >= 52:
@ -144,6 +149,10 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["a_mask"] = 0x0
for mask in masks:
file_info[mask] = i32(read(4))
assert isinstance(file_info["r_mask"], int)
assert isinstance(file_info["g_mask"], int)
assert isinstance(file_info["b_mask"], int)
assert isinstance(file_info["a_mask"], int)
file_info["rgb_mask"] = (
file_info["r_mask"],
file_info["g_mask"],
@ -164,24 +173,26 @@ class BmpImageFile(ImageFile.ImageFile):
self._size = file_info["width"], file_info["height"]
# ------- If color count was not found in the header, compute from bits
assert isinstance(file_info["bits"], int)
file_info["colors"] = (
file_info["colors"]
if file_info.get("colors", 0)
else (1 << file_info["bits"])
)
assert isinstance(file_info["colors"], int)
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None:
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
if not self.mode:
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
raise OSError(msg)
# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
if file_info["compression"] == self.BITFIELDS:
SUPPORTED = {
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
SUPPORTED: dict[int, list[tuple[int, ...]]] = {
32: [
(0xFF0000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
@ -213,12 +224,14 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["bits"] == 32
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
):
assert isinstance(file_info["rgba_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
self._mode = "RGBA" if "A" in raw_mode else self.mode
elif (
file_info["bits"] in (24, 16)
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
):
assert isinstance(file_info["rgb_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else:
msg = "Unsupported BMP bitfields layout"
@ -226,10 +239,13 @@ class BmpImageFile(ImageFile.ImageFile):
else:
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
elif file_info["compression"] == self.RAW:
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (self.RLE8, self.RLE4):
elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"],
self.COMPRESSIONS["RLE4"],
):
decoder_name = "bmp_rle"
else:
msg = f"Unsupported BMP compression ({file_info['compression']})"
@ -242,6 +258,7 @@ class BmpImageFile(ImageFile.ImageFile):
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
raise OSError(msg)
else:
assert isinstance(file_info["palette_padding"], int)
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
grayscale = True
@ -269,10 +286,11 @@ class BmpImageFile(ImageFile.ImageFile):
# ---------------------------- Finally set the tile data for the plugin
self.info["compression"] = file_info["compression"]
args = [raw_mode]
args: list[Any] = [raw_mode]
if decoder_name == "bmp_rle":
args.append(file_info["compression"] == self.RLE4)
args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
else:
assert isinstance(file_info["width"], int)
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"])
self.tile = [

View File

@ -65,7 +65,7 @@ def has_ghostscript() -> bool:
return gs_binary is not False
def Ghostscript(tile, size, fp, scale=1, transparency=False):
def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image:
"""Render an image using Ghostscript"""
global gs_binary
if not has_ghostscript():
@ -338,7 +338,7 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "cannot determine EPS bounding box"
raise OSError(msg)
def _find_offset(self, fp):
def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]:
s = fp.read(4)
if s == b"%!PS":
@ -361,7 +361,9 @@ class EpsImageFile(ImageFile.ImageFile):
return length, offset
def load(self, scale=1, transparency=False):
def load(
self, scale: int = 1, transparency: bool = False
) -> Image.core.PixelAccess | None:
# Load EPS via Ghostscript
if self.tile:
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)

View File

@ -45,7 +45,7 @@ class FliImageFile(ImageFile.ImageFile):
format_description = "Autodesk FLI/FLC Animation"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
# HEAD
s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"):
@ -83,7 +83,7 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF1FA:
# look for palette chunk
number_of_subchunks = i16(s, 6)
chunk_size = None
chunk_size: int | None = None
for _ in range(number_of_subchunks):
if chunk_size is not None:
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
@ -96,8 +96,9 @@ class FliImageFile(ImageFile.ImageFile):
if not chunk_size:
break
palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
self.palette = ImagePalette.raw("RGB", b"".join(palette))
self.palette = ImagePalette.raw(
"RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette)
)
# set things up to decode first frame
self.__frame = -1
@ -105,7 +106,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__rewind = self.fp.tell()
self.seek(0)
def _palette(self, palette, shift):
def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None:
# load palette
i = 0

View File

@ -231,7 +231,7 @@ class FpxImageFile(ImageFile.ImageFile):
self._fp = self.fp
self.fp = None
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if not self.fp:
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])

View File

@ -88,7 +88,7 @@ class GbrImageFile(ImageFile.ImageFile):
# Data is an uncompressed block of w * h * bytes/pixel
self._data_size = width * height * color_depth
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if not self.im:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size))

View File

@ -34,11 +34,13 @@ MAGIC = b"icns"
HEADERSIZE = 8
def nextheader(fobj):
def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]:
return struct.unpack(">4sI", fobj.read(HEADERSIZE))
def read_32t(fobj, start_length, size):
def read_32t(
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
) -> dict[str, Image.Image]:
# The 128x128 icon seems to have an extra header for some reason.
(start, length) = start_length
fobj.seek(start)
@ -49,7 +51,9 @@ def read_32t(fobj, start_length, size):
return read_32(fobj, (start + 4, length - 4), size)
def read_32(fobj, start_length, size):
def read_32(
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
) -> dict[str, Image.Image]:
"""
Read a 32bit RGB icon resource. Seems to be either uncompressed or
an RLE packbits-like scheme.
@ -72,14 +76,14 @@ def read_32(fobj, start_length, size):
byte = fobj.read(1)
if not byte:
break
byte = byte[0]
if byte & 0x80:
blocksize = byte - 125
byte_int = byte[0]
if byte_int & 0x80:
blocksize = byte_int - 125
byte = fobj.read(1)
for i in range(blocksize):
data.append(byte)
else:
blocksize = byte + 1
blocksize = byte_int + 1
data.append(fobj.read(blocksize))
bytesleft -= blocksize
if bytesleft <= 0:
@ -92,7 +96,9 @@ def read_32(fobj, start_length, size):
return {"RGB": im}
def read_mk(fobj, start_length, size):
def read_mk(
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
) -> dict[str, Image.Image]:
# Alpha masks seem to be uncompressed
start = start_length[0]
fobj.seek(start)
@ -102,10 +108,14 @@ def read_mk(fobj, start_length, size):
return {"A": band}
def read_png_or_jpeg2000(fobj, start_length, size):
def read_png_or_jpeg2000(
fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
) -> dict[str, Image.Image]:
(start, length) = start_length
fobj.seek(start)
sig = fobj.read(12)
im: Image.Image
if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
fobj.seek(start)
im = PngImagePlugin.PngImageFile(fobj)
@ -164,12 +174,12 @@ class IcnsFile:
],
}
def __init__(self, fobj):
def __init__(self, fobj: IO[bytes]) -> None:
"""
fobj is a file-like object as an icns resource
"""
# signature : (start, length)
self.dct = dct = {}
self.dct = {}
self.fobj = fobj
sig, filesize = nextheader(fobj)
if not _accept(sig):
@ -183,11 +193,11 @@ class IcnsFile:
raise SyntaxError(msg)
i += HEADERSIZE
blocksize -= HEADERSIZE
dct[sig] = (i, blocksize)
self.dct[sig] = (i, blocksize)
fobj.seek(blocksize, io.SEEK_CUR)
i += blocksize
def itersizes(self):
def itersizes(self) -> list[tuple[int, int, int]]:
sizes = []
for size, fmts in self.SIZES.items():
for fmt, reader in fmts:
@ -196,14 +206,14 @@ class IcnsFile:
break
return sizes
def bestsize(self):
def bestsize(self) -> tuple[int, int, int]:
sizes = self.itersizes()
if not sizes:
msg = "No 32bit icon resources found"
raise SyntaxError(msg)
return max(sizes)
def dataforsize(self, size):
def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]:
"""
Get an icon resource as {channel: array}. Note that
the arrays are bottom-up like windows bitmaps and will likely
@ -216,18 +226,20 @@ class IcnsFile:
dct.update(reader(self.fobj, desc, size))
return dct
def getimage(self, size=None):
def getimage(
self, size: tuple[int, int] | tuple[int, int, int] | None = None
) -> Image.Image:
if size is None:
size = self.bestsize()
if len(size) == 2:
elif len(size) == 2:
size = (size[0], size[1], 1)
channels = self.dataforsize(size)
im = channels.get("RGBA", None)
im = channels.get("RGBA")
if im:
return im
im = channels.get("RGB").copy()
im = channels["RGB"].copy()
try:
im.putalpha(channels["A"])
except KeyError:
@ -268,7 +280,7 @@ class IcnsImageFile(ImageFile.ImageFile):
return self._size
@size.setter
def size(self, value):
def size(self, value) -> None:
info_size = value
if info_size not in self.info["sizes"] and len(info_size) == 2:
info_size = (info_size[0], info_size[1], 1)
@ -287,7 +299,7 @@ class IcnsImageFile(ImageFile.ImageFile):
raise ValueError(msg)
self._size = value
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if len(self.size) == 3:
self.best_size = self.size
self.size = (

View File

@ -25,7 +25,7 @@ from __future__ import annotations
import warnings
from io import BytesIO
from math import ceil, log
from typing import IO
from typing import IO, NamedTuple
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16
@ -119,8 +119,22 @@ def _accept(prefix: bytes) -> bool:
return prefix[:4] == _MAGIC
class IconHeader(NamedTuple):
width: int
height: int
nb_color: int
reserved: int
planes: int
bpp: int
size: int
offset: int
dim: tuple[int, int]
square: int
color_depth: int
class IcoFile:
def __init__(self, buf):
def __init__(self, buf: IO[bytes]) -> None:
"""
Parse image from file-like object containing ico file data
"""
@ -141,55 +155,48 @@ class IcoFile:
for i in range(self.nb_items):
s = buf.read(16)
icon_header = {
"width": s[0],
"height": s[1],
"nb_color": s[2], # No. of colors in image (0 if >=8bpp)
"reserved": s[3],
"planes": i16(s, 4),
"bpp": i16(s, 6),
"size": i32(s, 8),
"offset": i32(s, 12),
}
# See Wikipedia
for j in ("width", "height"):
if not icon_header[j]:
icon_header[j] = 256
width = s[0] or 256
height = s[1] or 256
# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
icon_header["color_depth"] = (
icon_header["bpp"]
or (
icon_header["nb_color"] != 0
and ceil(log(icon_header["nb_color"], 2))
)
or 256
# No. of colors in image (0 if >=8bpp)
nb_color = s[2]
bpp = i16(s, 6)
icon_header = IconHeader(
width=width,
height=height,
nb_color=nb_color,
reserved=s[3],
planes=i16(s, 4),
bpp=i16(s, 6),
size=i32(s, 8),
offset=i32(s, 12),
dim=(width, height),
square=width * height,
# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256,
)
icon_header["dim"] = (icon_header["width"], icon_header["height"])
icon_header["square"] = icon_header["width"] * icon_header["height"]
self.entry.append(icon_header)
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
self.entry = sorted(self.entry, key=lambda x: x.color_depth)
# ICO images are usually squares
self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True)
def sizes(self):
def sizes(self) -> set[tuple[int, int]]:
"""
Get a list of all available icon sizes and color depths.
Get a set of all available icon sizes and color depths.
"""
return {(h["width"], h["height"]) for h in self.entry}
return {(h.width, h.height) for h in self.entry}
def getentryindex(self, size, bpp=False):
def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
for i, h in enumerate(self.entry):
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
if size == h.dim and (bpp is False or bpp == h.color_depth):
return i
return 0
def getimage(self, size, bpp=False):
def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image:
"""
Get an image from the icon
"""
@ -202,9 +209,9 @@ class IcoFile:
header = self.entry[idx]
self.buf.seek(header["offset"])
self.buf.seek(header.offset)
data = self.buf.read(8)
self.buf.seek(header["offset"])
self.buf.seek(header.offset)
im: Image.Image
if data[:8] == PngImagePlugin._MAGIC:
@ -222,8 +229,7 @@ class IcoFile:
im.tile[0] = d, (0, 0) + im.size, o, a
# figure out where AND mask image starts
bpp = header["bpp"]
if 32 == bpp:
if header.bpp == 32:
# 32-bit color depth icon image allows semitransparent areas
# PIL's DIB format ignores transparency bits, recover them.
# The DIB is packed in BGRX byte order where X is the alpha
@ -253,7 +259,7 @@ class IcoFile:
# padded row size * height / bits per char
total_bytes = int((w * im.size[1]) / 8)
and_mask_offset = header["offset"] + header["size"] - total_bytes
and_mask_offset = header.offset + header.size - total_bytes
self.buf.seek(and_mask_offset)
mask_data = self.buf.read(total_bytes)
@ -307,7 +313,7 @@ class IcoImageFile(ImageFile.ImageFile):
def _open(self) -> None:
self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"]
self.size = self.ico.entry[0].dim
self.load()
@property
@ -321,7 +327,7 @@ class IcoImageFile(ImageFile.ImageFile):
raise ValueError(msg)
self._size = value
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if self.im is not None and self.im.size == self.size:
# Already loaded
return Image.Image.load(self)
@ -341,6 +347,7 @@ class IcoImageFile(ImageFile.ImageFile):
self.info["sizes"] = set(sizes)
self.size = im.size
return None
def load_seek(self, pos: int) -> None:
# Flag the ImageFile.Parser so that it

View File

@ -63,7 +63,6 @@ from . import (
)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._typing import StrOrBytesPath, TypeGuard
from ._util import DeferredError, is_path
ElementTree: ModuleType | None
@ -220,6 +219,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
if TYPE_CHECKING:
from . import ImageFile, ImagePalette
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
OPEN: dict[
str,
@ -1395,7 +1395,7 @@ class Image:
def getcolors(
self, maxcolors: int = 256
) -> list[tuple[int, int]] | list[tuple[int, float]] | None:
) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None:
"""
Returns a list of colors used in this image.
@ -1412,7 +1412,7 @@ class Image:
self.load()
if self.mode in ("1", "L", "P"):
h = self.im.histogram()
out = [(h[i], i) for i in range(256) if h[i]]
out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]]
if len(out) > maxcolors:
return None
return out
@ -1886,7 +1886,7 @@ class Image:
def point(
self,
lut: Sequence[float] | Callable[[int], float] | ImagePointHandler,
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
mode: str | None = None,
) -> Image:
"""
@ -1996,7 +1996,7 @@ class Image:
def putdata(
self,
data: Sequence[float] | Sequence[Sequence[int]],
data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
scale: float = 1.0,
offset: float = 0.0,
) -> None:
@ -2203,7 +2203,7 @@ class Image:
def resize(
self,
size: tuple[int, int],
size: tuple[int, int] | list[int] | NumpyArray,
resample: int | None = None,
box: tuple[float, float, float, float] | None = None,
reducing_gap: float | None = None,
@ -2211,7 +2211,7 @@ class Image:
"""
Returns a resized copy of this image.
:param size: The requested size in pixels, as a 2-tuple:
:param size: The requested size in pixels, as a tuple or array:
(width, height).
:param resample: An optional resampling filter. This can be
one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`,
@ -2276,6 +2276,7 @@ class Image:
if box is None:
box = (0, 0) + self.size
size = tuple(size)
if self.size == size and box == (0, 0) + self.size:
return self.copy()
@ -3302,7 +3303,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
def fromqimage(im):
def fromqimage(im) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image"""
from . import ImageQt
@ -3312,7 +3313,7 @@ def fromqimage(im):
return ImageQt.fromqimage(im)
def fromqpixmap(im):
def fromqpixmap(im) -> ImageFile.ImageFile:
"""Creates an image instance from a QPixmap image"""
from . import ImageQt
@ -3883,7 +3884,7 @@ class Exif(_ExifBase):
# returns a dict with any single item tuples/lists as individual values
return {k: self._fixup(v) for k, v in src_dict.items()}
def _get_ifd_dict(self, offset, group=None):
def _get_ifd_dict(self, offset: int, group=None):
try:
# an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted.
@ -3897,7 +3898,7 @@ class Exif(_ExifBase):
info.load(self.fp)
return self._fixup_dict(info)
def _get_head(self):
def _get_head(self) -> bytes:
version = b"\x2B" if self.bigtiff else b"\x2A"
if self.endian == "<":
head = b"II" + version + b"\x00" + o32le(8)
@ -4118,16 +4119,16 @@ class Exif(_ExifBase):
keys.update(self._info)
return len(keys)
def __getitem__(self, tag):
def __getitem__(self, tag: int):
if self._info is not None and tag not in self._data and tag in self._info:
self._data[tag] = self._fixup(self._info[tag])
del self._info[tag]
return self._data[tag]
def __contains__(self, tag) -> bool:
def __contains__(self, tag: object) -> bool:
return tag in self._data or (self._info is not None and tag in self._info)
def __setitem__(self, tag, value) -> None:
def __setitem__(self, tag: int, value) -> None:
if self._info is not None and tag in self._info:
del self._info[tag]
self._data[tag] = value

View File

@ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError:
raise _get_oserror(error, encoder=False)
def _tilesort(t):
def _tilesort(t) -> int:
# sort on offset
return t[2]
@ -161,7 +161,7 @@ class ImageFile(Image.Image):
return Image.MIME.get(self.format.upper())
return None
def __setstate__(self, state):
def __setstate__(self, state) -> None:
self.tile = []
super().__setstate__(state)
@ -333,14 +333,14 @@ class ImageFile(Image.Image):
# def load_read(self, read_bytes: int) -> bytes:
# pass
def _seek_check(self, frame):
def _seek_check(self, frame: int) -> bool:
if (
frame < self._min_frame
# Only check upper limit on frames if additional seek operations
# are not required to do so
or (
not (hasattr(self, "_n_frames") and self._n_frames is None)
and frame >= self.n_frames + self._min_frame
and frame >= getattr(self, "n_frames") + self._min_frame
)
):
msg = "attempt to seek outside sequence"
@ -370,7 +370,7 @@ class StubImageFile(ImageFile):
msg = "StubImageFile subclass must implement _open"
raise NotImplementedError(msg)
def load(self):
def load(self) -> Image.core.PixelAccess | None:
loader = self._load()
if loader is None:
msg = f"cannot find loader for this {self.format} file"
@ -378,7 +378,7 @@ class StubImageFile(ImageFile):
image = loader.load(self)
assert image is not None
# become the other object (!)
self.__class__ = image.__class__
self.__class__ = image.__class__ # type: ignore[assignment]
self.__dict__ = image.__dict__
return image.load()
@ -396,8 +396,8 @@ class Parser:
incremental = None
image: Image.Image | None = None
data = None
decoder = None
data: bytes | None = None
decoder: Image.core.ImagingDecoder | PyDecoder | None = None
offset = 0
finished = 0
@ -409,7 +409,7 @@ class Parser:
"""
assert self.data is None, "cannot reuse parsers"
def feed(self, data):
def feed(self, data: bytes) -> None:
"""
(Consumer) Feed data to the parser.
@ -485,13 +485,13 @@ class Parser:
self.image = im
def __enter__(self):
def __enter__(self) -> Parser:
return self
def __exit__(self, *args: object) -> None:
self.close()
def close(self):
def close(self) -> Image.Image:
"""
(Consumer) Close the stream.
@ -525,7 +525,7 @@ class Parser:
# --------------------------------------------------------------------
def _save(im, fp, tile, bufsize=0) -> None:
def _save(im, fp, tile, bufsize: int = 0) -> None:
"""Helper to save image based on tile list
:param im: Image object.
@ -553,7 +553,9 @@ def _save(im, fp, tile, bufsize=0) -> None:
fp.flush()
def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
def _encode_tile(
im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None
) -> None:
for encoder_name, extents, offset, args in tile:
if offset > 0:
fp.seek(offset)
@ -580,7 +582,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None):
encoder.cleanup()
def _safe_read(fp, size):
def _safe_read(fp: IO[bytes], size: int) -> bytes:
"""
Reads large blocks in a safe way. Unlike fp.read(n), this function
doesn't trust the user. If the requested size is larger than
@ -601,18 +603,18 @@ def _safe_read(fp, size):
msg = "Truncated File Read"
raise OSError(msg)
return data
data = []
blocks: list[bytes] = []
remaining_size = size
while remaining_size > 0:
block = fp.read(min(remaining_size, SAFEBLOCK))
if not block:
break
data.append(block)
blocks.append(block)
remaining_size -= len(block)
if sum(len(d) for d in data) < size:
if sum(len(block) for block in blocks) < size:
msg = "Truncated File Read"
raise OSError(msg)
return b"".join(data)
return b"".join(blocks)
class PyCodecState:
@ -629,18 +631,18 @@ class PyCodecState:
class PyCodec:
fd: IO[bytes] | None
def __init__(self, mode, *args):
self.im = None
def __init__(self, mode: str, *args: Any) -> None:
self.im: Image.core.ImagingCore | None = None
self.state = PyCodecState()
self.fd = None
self.mode = mode
self.init(args)
def init(self, args):
def init(self, args: tuple[Any, ...]) -> None:
"""
Override to perform codec specific initialization
:param args: Array of args items from the tile entry
:param args: Tuple of arg items from the tile entry
:returns: None
"""
self.args = args
@ -653,7 +655,7 @@ class PyCodec:
"""
pass
def setfd(self, fd):
def setfd(self, fd: IO[bytes]) -> None:
"""
Called from ImageFile to set the Python file-like object
@ -662,7 +664,7 @@ class PyCodec:
"""
self.fd = fd
def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None:
def setimage(self, im, extents=None):
"""
Called from ImageFile to set the core output image for the codec
@ -793,7 +795,7 @@ class PyEncoder(PyCodec):
self.fd.write(data)
return bytes_consumed, errcode
def encode_to_file(self, fh, bufsize):
def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
"""
:param fh: File handle.
:param bufsize: Buffer size.

View File

@ -19,11 +19,14 @@ from __future__ import annotations
import sys
from io import BytesIO
from typing import Callable
from typing import TYPE_CHECKING, Callable
from . import Image
from ._util import is_path
if TYPE_CHECKING:
from . import ImageFile
qt_version: str | None
qt_versions = [
["6", "PyQt6"],
@ -90,11 +93,11 @@ def fromqimage(im):
return Image.open(b)
def fromqpixmap(im):
def fromqpixmap(im) -> ImageFile.ImageFile:
return fromqimage(im)
def align8to32(bytes, width, mode):
def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
"""
converts each scanline of data from 8 bit to 32 bit aligned
"""
@ -172,7 +175,7 @@ def _toqclass_helper(im):
if qt_is_installed:
class ImageQt(QImage):
def __init__(self, im):
def __init__(self, im) -> None:
"""
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
class.

View File

@ -33,7 +33,7 @@ class Iterator:
:param im: An image object.
"""
def __init__(self, im: Image.Image):
def __init__(self, im: Image.Image) -> None:
if not hasattr(im, "seek"):
msg = "im must have seek method"
raise AttributeError(msg)

View File

@ -26,7 +26,7 @@ from . import Image
_viewers = []
def register(viewer, order: int = 1) -> None:
def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None:
"""
The :py:func:`register` function is used to register additional viewers::
@ -40,11 +40,8 @@ def register(viewer, order: int = 1) -> None:
Zero or a negative integer to prepend this viewer to the list,
a positive integer to append it.
"""
try:
if issubclass(viewer, Viewer):
viewer = viewer()
except TypeError:
pass # raised if viewer wasn't a class
if isinstance(viewer, type) and issubclass(viewer, Viewer):
viewer = viewer()
if order > 0:
_viewers.append(viewer)
else:

View File

@ -28,7 +28,7 @@ from __future__ import annotations
import tkinter
from io import BytesIO
from typing import Any
from typing import TYPE_CHECKING, Any, cast
from . import Image, ImageFile
@ -61,7 +61,9 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
return Image.open(source)
def _pyimagingtkcall(command, photo, id):
def _pyimagingtkcall(
command: str, photo: PhotoImage | tkinter.PhotoImage, id: int
) -> None:
tk = photo.tk
try:
tk.call(command, photo, id)
@ -215,11 +217,14 @@ class BitmapImage:
:param image: A PIL image.
"""
def __init__(self, image=None, **kw):
def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
if image is None:
msg = "Image is required"
raise ValueError(msg)
self.__mode = image.mode
self.__size = image.size
@ -278,18 +283,23 @@ def getimage(photo: PhotoImage) -> Image.Image:
return im
def _show(image, title):
def _show(image: Image.Image, title: str | None) -> None:
"""Helper for the Image.show method."""
class UI(tkinter.Label):
def __init__(self, master, im):
def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None:
self.image: BitmapImage | PhotoImage
if im.mode == "1":
self.image = BitmapImage(im, foreground="white", master=master)
else:
self.image = PhotoImage(im, master=master)
super().__init__(master, image=self.image, bg="black", bd=0)
if TYPE_CHECKING:
image = cast(tkinter._Image, self.image)
else:
image = self.image
super().__init__(master, image=image, bg="black", bd=0)
if not tkinter._default_root:
if not getattr(tkinter, "_default_root"):
msg = "tkinter not initialized"
raise OSError(msg)
top = tkinter.Toplevel()

View File

@ -70,11 +70,14 @@ class Dib:
"""
def __init__(
self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None
self, image: Image.Image | str, size: tuple[int, int] | None = None
) -> None:
if isinstance(image, str):
mode = image
image = ""
if size is None:
msg = "If first argument is mode, size is required"
raise ValueError(msg)
else:
mode = image.mode
size = image.size
@ -105,7 +108,12 @@ class Dib:
result = self.image.expose(handle)
return result
def draw(self, handle, dst, src=None):
def draw(
self,
handle,
dst: tuple[int, int, int, int],
src: tuple[int, int, int, int] | None = None,
):
"""
Same as expose, but allows you to specify where to draw the image, and
what part of it to draw.
@ -115,7 +123,7 @@ class Dib:
the destination have different sizes, the image is resized as
necessary.
"""
if not src:
if src is None:
src = (0, 0) + self.size
if isinstance(handle, HWND):
dc = self.image.getdc(handle)
@ -202,22 +210,22 @@ class Window:
title, self.__dispatcher, width or 0, height or 0
)
def __dispatcher(self, action, *args):
def __dispatcher(self, action: str, *args):
return getattr(self, f"ui_handle_{action}")(*args)
def ui_handle_clear(self, dc, x0, y0, x1, y1):
def ui_handle_clear(self, dc, x0, y0, x1, y1) -> None:
pass
def ui_handle_damage(self, x0, y0, x1, y1):
def ui_handle_damage(self, x0, y0, x1, y1) -> None:
pass
def ui_handle_destroy(self) -> None:
pass
def ui_handle_repair(self, dc, x0, y0, x1, y1):
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None:
pass
def ui_handle_resize(self, width, height):
def ui_handle_resize(self, width, height) -> None:
pass
def mainloop(self) -> None:
@ -227,12 +235,12 @@ class Window:
class ImageWindow(Window):
"""Create an image window which displays the given image."""
def __init__(self, image, title="PIL"):
def __init__(self, image, title: str = "PIL") -> None:
if not isinstance(image, Dib):
image = Dib(image)
self.image = image
width, height = image.size
super().__init__(title, width=width, height=height)
def ui_handle_repair(self, dc, x0, y0, x1, y1):
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None:
self.image.draw(dc, (x0, y0, x1, y1))

View File

@ -18,6 +18,7 @@ from __future__ import annotations
from collections.abc import Sequence
from io import BytesIO
from typing import cast
from . import Image, ImageFile
from ._binary import i16be as i16
@ -148,7 +149,7 @@ class IptcImageFile(ImageFile.ImageFile):
if tag == (8, 10):
self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
return ImageFile.ImageFile.load(self)
@ -176,6 +177,7 @@ class IptcImageFile(ImageFile.ImageFile):
with Image.open(o) as _im:
_im.load()
self.im = _im.im
return None
Image.register_open(IptcImageFile.format, IptcImageFile)
@ -183,7 +185,7 @@ Image.register_open(IptcImageFile.format, IptcImageFile)
Image.register_extension(IptcImageFile.format, ".iim")
def getiptcinfo(im):
def getiptcinfo(im: ImageFile.ImageFile):
"""
Get IPTC information from TIFF, JPEG, or IPTC file.
@ -220,16 +222,17 @@ def getiptcinfo(im):
class FakeImage:
pass
im = FakeImage()
im.__class__ = IptcImageFile
fake_im = FakeImage()
fake_im.__class__ = IptcImageFile # type: ignore[assignment]
iptc_im = cast(IptcImageFile, fake_im)
# parse the IPTC information chunk
im.info = {}
im.fp = BytesIO(data)
iptc_im.info = {}
iptc_im.fp = BytesIO(data)
try:
im._open()
iptc_im._open()
except (IndexError, KeyError):
pass # expected failure
return im.info
return iptc_im.info

View File

@ -29,7 +29,7 @@ class BoxReader:
and to easily step into and read sub-boxes.
"""
def __init__(self, fp, length=-1):
def __init__(self, fp: IO[bytes], length: int = -1) -> None:
self.fp = fp
self.has_length = length >= 0
self.length = length
@ -97,7 +97,7 @@ class BoxReader:
return tbox
def _parse_codestream(fp) -> tuple[tuple[int, int], str]:
def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]:
"""Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@ -137,7 +137,15 @@ def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
return (254 * num * (10**exp)) / (10000 * denom)
def _parse_jp2_header(fp):
def _parse_jp2_header(
fp: IO[bytes],
) -> tuple[
tuple[int, int],
str,
str | None,
tuple[float, float] | None,
ImagePalette.ImagePalette | None,
]:
"""Parse the JP2 header box to extract size, component count,
color space information, and optionally DPI information,
returning a (size, mode, mimetype, dpi) tuple."""
@ -155,6 +163,7 @@ def _parse_jp2_header(fp):
elif tbox == b"ftyp":
if reader.read_fields(">4s")[0] == b"jpx ":
mimetype = "image/jpx"
assert header is not None
size = None
mode = None
@ -168,6 +177,9 @@ def _parse_jp2_header(fp):
if tbox == b"ihdr":
height, width, nc, bpc = header.read_fields(">IIHB")
assert isinstance(height, int)
assert isinstance(width, int)
assert isinstance(bpc, int)
size = (width, height)
if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16"
@ -185,11 +197,21 @@ def _parse_jp2_header(fp):
mode = "CMYK"
elif tbox == b"pclr" and mode in ("L", "LA"):
ne, npc = header.read_fields(">HB")
bitdepths = header.read_fields(">" + ("B" * npc))
if max(bitdepths) <= 8:
assert isinstance(ne, int)
assert isinstance(npc, int)
max_bitdepth = 0
for bitdepth in header.read_fields(">" + ("B" * npc)):
assert isinstance(bitdepth, int)
if bitdepth > max_bitdepth:
max_bitdepth = bitdepth
if max_bitdepth <= 8:
palette = ImagePalette.ImagePalette()
for i in range(ne):
palette.getcolor(header.read_fields(">" + ("B" * npc)))
color: list[int] = []
for value in header.read_fields(">" + ("B" * npc)):
assert isinstance(value, int)
color.append(value)
palette.getcolor(tuple(color))
mode = "P" if mode == "L" else "PA"
elif tbox == b"res ":
res = header.read_boxes()
@ -197,6 +219,12 @@ def _parse_jp2_header(fp):
tres = res.next_box_type()
if tres == b"resc":
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
assert isinstance(vrcn, int)
assert isinstance(vrcd, int)
assert isinstance(hrcn, int)
assert isinstance(hrcd, int)
assert isinstance(vrce, int)
assert isinstance(hrce, int)
hres = _res_to_dpi(hrcn, hrcd, hrce)
vres = _res_to_dpi(vrcn, vrcd, vrce)
if hres is not None and vres is not None:
@ -299,7 +327,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
def reduce(self, value):
self._reduce = value
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if self.tile and self._reduce:
power = 1 << self._reduce
adjust = power >> 1

View File

@ -60,7 +60,7 @@ def Skip(self: JpegImageFile, marker: int) -> None:
ImageFile._safe_read(self.fp, n)
def APP(self, marker):
def APP(self: JpegImageFile, marker: int) -> None:
#
# Application marker. Store these in the APP dictionary.
# Also look for well-known application markers.
@ -133,13 +133,14 @@ def APP(self, marker):
offset += 4
data = s[offset : offset + size]
if code == 0x03ED: # ResolutionInfo
data = {
photoshop[code] = {
"XResolution": i32(data, 0) / 65536,
"DisplayedUnitsX": i16(data, 4),
"YResolution": i32(data, 8) / 65536,
"DisplayedUnitsY": i16(data, 12),
}
photoshop[code] = data
else:
photoshop[code] = data
offset += size
offset += offset & 1 # align
except struct.error:
@ -338,6 +339,7 @@ class JpegImageFile(ImageFile.ImageFile):
# Create attributes
self.bits = self.layers = 0
self._exif_offset = 0
# JPEG specifics (internal)
self.layer = []
@ -498,17 +500,17 @@ class JpegImageFile(ImageFile.ImageFile):
):
self.info["dpi"] = 72, 72
def _getmp(self):
def _getmp(self) -> dict[int, Any] | None:
return _getmp(self)
def _getexif(self) -> dict[str, Any] | None:
def _getexif(self: JpegImageFile) -> dict[str, Any] | None:
if "exif" not in self.info:
return None
return self.getexif()._get_merged_dict()
def _getmp(self):
def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
# Extract MP information. This method was inspired by the "highly
# experimental" _getexif version that's been in use for years now,
# itself based on the ImageFileDirectory class in the TIFF plugin.
@ -616,7 +618,7 @@ samplings = {
# fmt: on
def get_sampling(im):
def get_sampling(im: Image.Image) -> int:
# There's no subsampling when images have only 1 layer
# (grayscale images) or when they are CMYK (4 layers),
# so set subsampling to the default value.
@ -624,7 +626,7 @@ def get_sampling(im):
# NOTE: currently Pillow can't encode JPEG to YCCK format.
# If YCCK support is added in the future, subsampling code will have
# to be updated (here and in JpegEncode.c) to deal with 4 layers.
if not hasattr(im, "layers") or im.layers in (1, 4):
if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
return -1
sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
return samplings.get(sampling, -1)
@ -683,7 +685,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
raise ValueError(msg)
subsampling = get_sampling(im)
def validate_qtables(qtables):
def validate_qtables(
qtables: (
str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None
)
) -> list[list[int]] | None:
if qtables is None:
return qtables
if isinstance(qtables, str):
@ -713,12 +719,12 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if len(table) != 64:
msg = "Invalid quantization table"
raise TypeError(msg)
table = array.array("H", table)
table_array = array.array("H", table)
except TypeError as e:
msg = "Invalid quantization table"
raise ValueError(msg) from e
else:
qtables[idx] = list(table)
qtables[idx] = list(table_array)
return qtables
if qtables == "keep":
@ -825,11 +831,11 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
##
# Factory for making JPEG and MPO instances
def jpeg_factory(fp=None, filename=None):
def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None):
im = JpegImageFile(fp, filename)
try:
mpheader = im._getmp()
if mpheader[45057] > 1:
if mpheader is not None and mpheader[45057] > 1:
for segment, content in im.applist:
if segment == "APP1" and b' hdrgm:Version="' in content:
# Ultra HDR images are not yet supported

View File

@ -21,7 +21,7 @@ from __future__ import annotations
import os
import struct
from typing import IO
from typing import IO, Any, cast
from . import (
Image,
@ -111,8 +111,11 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open()
def _after_jpeg_open(self, mpheader=None):
def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
self.mpinfo = mpheader if mpheader is not None else self._getmp()
if self.mpinfo is None:
msg = "Image appears to be a malformed MPO file"
raise ValueError(msg)
self.n_frames = self.mpinfo[0xB001]
self.__mpoffsets = [
mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
@ -159,7 +162,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
return self.__frame
@staticmethod
def adopt(jpeg_instance, mpheader=None):
def adopt(
jpeg_instance: JpegImagePlugin.JpegImageFile,
mpheader: dict[int, Any] | None = None,
) -> MpoImageFile:
"""
Transform the instance of JpegImageFile into
an instance of MpoImageFile.
@ -171,8 +177,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
double call to _open.
"""
jpeg_instance.__class__ = MpoImageFile
jpeg_instance._after_jpeg_open(mpheader)
return jpeg_instance
mpo_instance = cast(MpoImageFile, jpeg_instance)
mpo_instance._after_jpeg_open(mpheader)
return mpo_instance
# ---------------------------------------------------------------------

View File

@ -174,12 +174,15 @@ def _write_image(im, filename, existing_pdf, image_refs):
return image_ref, procset
def _save(im, fp, filename, save_all=False):
def _save(
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
) -> None:
is_appending = im.encoderinfo.get("append", False)
filename_str = filename.decode() if isinstance(filename, bytes) else filename
if is_appending:
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b")
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b")
else:
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b")
existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b")
dpi = im.encoderinfo.get("dpi")
if dpi:
@ -228,12 +231,7 @@ def _save(im, fp, filename, save_all=False):
for im in ims:
im_number_of_pages = 1
if save_all:
try:
im_number_of_pages = im.n_frames
except AttributeError:
# Image format does not have n_frames.
# It is a single frame image
pass
im_number_of_pages = getattr(im, "n_frames", 1)
number_of_pages += im_number_of_pages
for i in range(im_number_of_pages):
image_refs.append(existing_pdf.next_object_id(0))
@ -250,7 +248,9 @@ def _save(im, fp, filename, save_all=False):
page_number = 0
for i, im_sequence in enumerate(ims):
im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
im_pages: ImageSequence.Iterator | list[Image.Image] = (
ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
)
for im in im_pages:
image_ref, procset = _write_image(im, filename, existing_pdf, image_refs)

View File

@ -8,7 +8,7 @@ import os
import re
import time
import zlib
from typing import TYPE_CHECKING, Any, NamedTuple, Union
from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@ -62,7 +62,7 @@ PDFDocEncoding = {
}
def decode_text(b):
def decode_text(b: bytes) -> str:
if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be")
else:
@ -99,7 +99,7 @@ class IndirectReference(IndirectReferenceTuple):
assert isinstance(other, IndirectReference)
return other.object_id == self.object_id and other.generation == self.generation
def __ne__(self, other):
def __ne__(self, other: object) -> bool:
return not (self == other)
def __hash__(self) -> int:
@ -112,13 +112,17 @@ class IndirectObjectDef(IndirectReference):
class XrefTable:
def __init__(self):
self.existing_entries = {} # object ID => (offset, generation)
self.new_entries = {} # object ID => (offset, generation)
def __init__(self) -> None:
self.existing_entries: dict[int, tuple[int, int]] = (
{}
) # object ID => (offset, generation)
self.new_entries: dict[int, tuple[int, int]] = (
{}
) # object ID => (offset, generation)
self.deleted_entries = {0: 65536} # object ID => generation
self.reading_finished = False
def __setitem__(self, key, value):
def __setitem__(self, key: int, value: tuple[int, int]) -> None:
if self.reading_finished:
self.new_entries[key] = value
else:
@ -126,13 +130,13 @@ class XrefTable:
if key in self.deleted_entries:
del self.deleted_entries[key]
def __getitem__(self, key):
def __getitem__(self, key: int) -> tuple[int, int]:
try:
return self.new_entries[key]
except KeyError:
return self.existing_entries[key]
def __delitem__(self, key):
def __delitem__(self, key: int) -> None:
if key in self.new_entries:
generation = self.new_entries[key][1] + 1
del self.new_entries[key]
@ -146,7 +150,7 @@ class XrefTable:
msg = f"object ID {key} cannot be deleted because it doesn't exist"
raise IndexError(msg)
def __contains__(self, key):
def __contains__(self, key: int) -> bool:
return key in self.existing_entries or key in self.new_entries
def __len__(self) -> int:
@ -156,19 +160,19 @@ class XrefTable:
| set(self.deleted_entries.keys())
)
def keys(self):
def keys(self) -> set[int]:
return (
set(self.existing_entries.keys()) - set(self.deleted_entries.keys())
) | set(self.new_entries.keys())
def write(self, f):
def write(self, f: IO[bytes]) -> int:
keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys()))
deleted_keys = sorted(set(self.deleted_entries.keys()))
startxref = f.tell()
f.write(b"xref\n")
while keys:
# find a contiguous sequence of object IDs
prev = None
prev: int | None = None
for index, key in enumerate(keys):
if prev is None or prev + 1 == key:
prev = key
@ -178,7 +182,7 @@ class XrefTable:
break
else:
contiguous_keys = keys
keys = None
keys = []
f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))
for object_id in contiguous_keys:
if object_id in self.new_entries:
@ -202,7 +206,9 @@ class XrefTable:
class PdfName:
def __init__(self, name):
name: bytes
def __init__(self, name: PdfName | bytes | str) -> None:
if isinstance(name, PdfName):
self.name = name.name
elif isinstance(name, bytes):
@ -213,7 +219,7 @@ class PdfName:
def name_as_str(self) -> str:
return self.name.decode("us-ascii")
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
return (
isinstance(other, PdfName) and other.name == self.name
) or other == self.name
@ -225,7 +231,7 @@ class PdfName:
return f"{self.__class__.__name__}({repr(self.name)})"
@classmethod
def from_pdf_stream(cls, data):
def from_pdf_stream(cls, data: bytes) -> PdfName:
return cls(PdfParser.interpret_name(data))
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
@ -252,13 +258,13 @@ else:
class PdfDict(_DictBase):
def __setattr__(self, key, value):
def __setattr__(self, key: str, value: Any) -> None:
if key == "data":
collections.UserDict.__setattr__(self, key, value)
else:
self[key.encode("us-ascii")] = value
def __getattr__(self, key):
def __getattr__(self, key: str) -> str | time.struct_time:
try:
value = self[key.encode("us-ascii")]
except KeyError as e:
@ -300,7 +306,7 @@ class PdfDict(_DictBase):
class PdfBinary:
def __init__(self, data):
def __init__(self, data: list[int] | bytes) -> None:
self.data = data
def __bytes__(self) -> bytes:
@ -308,27 +314,27 @@ class PdfBinary:
class PdfStream:
def __init__(self, dictionary, buf):
def __init__(self, dictionary: PdfDict, buf: bytes) -> None:
self.dictionary = dictionary
self.buf = buf
def decode(self):
def decode(self) -> bytes:
try:
filter = self.dictionary.Filter
except AttributeError:
filter = self.dictionary[b"Filter"]
except KeyError:
return self.buf
if filter == b"FlateDecode":
try:
expected_length = self.dictionary.DL
except AttributeError:
expected_length = self.dictionary.Length
expected_length = self.dictionary[b"DL"]
except KeyError:
expected_length = self.dictionary[b"Length"]
return zlib.decompress(self.buf, bufsize=int(expected_length))
else:
msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
msg = f"stream filter {repr(filter)} unknown/unsupported"
raise NotImplementedError(msg)
def pdf_repr(x):
def pdf_repr(x: Any) -> bytes:
if x is True:
return b"true"
elif x is False:
@ -363,12 +369,19 @@ class PdfParser:
Supports PDF up to 1.4
"""
def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"):
def __init__(
self,
filename: str | None = None,
f: IO[bytes] | None = None,
buf: bytes | bytearray | None = None,
start_offset: int = 0,
mode: str = "rb",
) -> None:
if buf and f:
msg = "specify buf or f or filename, but not both buf and f"
raise RuntimeError(msg)
self.filename = filename
self.buf = buf
self.buf: bytes | bytearray | mmap.mmap | None = buf
self.f = f
self.start_offset = start_offset
self.should_close_buf = False
@ -377,12 +390,16 @@ class PdfParser:
self.f = f = open(filename, mode)
self.should_close_file = True
if f is not None:
self.buf = buf = self.get_buf_from_file(f)
self.buf = self.get_buf_from_file(f)
self.should_close_buf = True
if not filename and hasattr(f, "name"):
self.filename = f.name
self.cached_objects = {}
if buf:
self.cached_objects: dict[IndirectReference, Any] = {}
self.root_ref: IndirectReference | None
self.info_ref: IndirectReference | None
self.pages_ref: IndirectReference | None
self.last_xref_section_offset: int | None
if self.buf:
self.read_pdf_info()
else:
self.file_size_total = self.file_size_this = 0
@ -390,12 +407,12 @@ class PdfParser:
self.root_ref = None
self.info = PdfDict()
self.info_ref = None
self.page_tree_root = {}
self.pages = []
self.orig_pages = []
self.page_tree_root = PdfDict()
self.pages: list[IndirectReference] = []
self.orig_pages: list[IndirectReference] = []
self.pages_ref = None
self.last_xref_section_offset = None
self.trailer_dict = {}
self.trailer_dict: dict[bytes, Any] = {}
self.xref_table = XrefTable()
self.xref_table.reading_finished = True
if f:
@ -412,10 +429,8 @@ class PdfParser:
self.seek_end()
def close_buf(self) -> None:
try:
if isinstance(self.buf, mmap.mmap):
self.buf.close()
except AttributeError:
pass
self.buf = None
def close(self) -> None:
@ -426,15 +441,19 @@ class PdfParser:
self.f = None
def seek_end(self) -> None:
assert self.f is not None
self.f.seek(0, os.SEEK_END)
def write_header(self) -> None:
assert self.f is not None
self.f.write(b"%PDF-1.4\n")
def write_comment(self, s):
def write_comment(self, s: str) -> None:
assert self.f is not None
self.f.write(f"% {s}\n".encode())
def write_catalog(self) -> IndirectReference:
assert self.f is not None
self.del_root()
self.root_ref = self.next_object_id(self.f.tell())
self.pages_ref = self.next_object_id(0)
@ -477,7 +496,10 @@ class PdfParser:
pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
self.orig_pages = []
def write_xref_and_trailer(self, new_root_ref=None):
def write_xref_and_trailer(
self, new_root_ref: IndirectReference | None = None
) -> None:
assert self.f is not None
if new_root_ref:
self.del_root()
self.root_ref = new_root_ref
@ -485,7 +507,10 @@ class PdfParser:
self.info_ref = self.write_obj(None, self.info)
start_xref = self.xref_table.write(self.f)
num_entries = len(self.xref_table)
trailer_dict = {b"Root": self.root_ref, b"Size": num_entries}
trailer_dict: dict[str | bytes, Any] = {
b"Root": self.root_ref,
b"Size": num_entries,
}
if self.last_xref_section_offset is not None:
trailer_dict[b"Prev"] = self.last_xref_section_offset
if self.info:
@ -497,16 +522,20 @@ class PdfParser:
+ b"\nstartxref\n%d\n%%%%EOF" % start_xref
)
def write_page(self, ref, *objs, **dict_obj):
if isinstance(ref, int):
ref = self.pages[ref]
def write_page(
self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any
) -> IndirectReference:
obj_ref = self.pages[ref] if isinstance(ref, int) else ref
if "Type" not in dict_obj:
dict_obj["Type"] = PdfName(b"Page")
if "Parent" not in dict_obj:
dict_obj["Parent"] = self.pages_ref
return self.write_obj(ref, *objs, **dict_obj)
return self.write_obj(obj_ref, *objs, **dict_obj)
def write_obj(self, ref, *objs, **dict_obj):
def write_obj(
self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any
) -> IndirectReference:
assert self.f is not None
f = self.f
if ref is None:
ref = self.next_object_id(f.tell())
@ -534,7 +563,7 @@ class PdfParser:
del self.xref_table[self.root[b"Pages"].object_id]
@staticmethod
def get_buf_from_file(f):
def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap:
if hasattr(f, "getbuffer"):
return f.getbuffer()
elif hasattr(f, "getvalue"):
@ -546,10 +575,15 @@ class PdfParser:
return b""
def read_pdf_info(self) -> None:
assert self.buf is not None
self.file_size_total = len(self.buf)
self.file_size_this = self.file_size_total - self.start_offset
self.read_trailer()
check_format_condition(
self.trailer_dict.get(b"Root") is not None, "Root is missing"
)
self.root_ref = self.trailer_dict[b"Root"]
assert self.root_ref is not None
self.info_ref = self.trailer_dict.get(b"Info", None)
self.root = PdfDict(self.read_indirect(self.root_ref))
if self.info_ref is None:
@ -560,12 +594,15 @@ class PdfParser:
check_format_condition(
self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog"
)
check_format_condition(b"Pages" in self.root, "/Pages missing in Root")
check_format_condition(
self.root.get(b"Pages") is not None, "/Pages missing in Root"
)
check_format_condition(
isinstance(self.root[b"Pages"], IndirectReference),
"/Pages in Root is not an indirect reference",
)
self.pages_ref = self.root[b"Pages"]
assert self.pages_ref is not None
self.page_tree_root = self.read_indirect(self.pages_ref)
self.pages = self.linearize_page_tree(self.page_tree_root)
# save the original list of page references
@ -573,7 +610,7 @@ class PdfParser:
# and we need to rewrite the pages and their list
self.orig_pages = self.pages[:]
def next_object_id(self, offset=None):
def next_object_id(self, offset: int | None = None) -> IndirectReference:
try:
# TODO: support reuse of deleted objects
reference = IndirectReference(max(self.xref_table.keys()) + 1, 0)
@ -623,12 +660,13 @@ class PdfParser:
re.DOTALL,
)
def read_trailer(self):
def read_trailer(self) -> None:
assert self.buf is not None
search_start_offset = len(self.buf) - 16384
if search_start_offset < self.start_offset:
search_start_offset = self.start_offset
m = self.re_trailer_end.search(self.buf, search_start_offset)
check_format_condition(m, "trailer end not found")
check_format_condition(m is not None, "trailer end not found")
# make sure we found the LAST trailer
last_match = m
while m:
@ -636,6 +674,7 @@ class PdfParser:
m = self.re_trailer_end.search(self.buf, m.start() + 16)
if not m:
m = last_match
assert m is not None
trailer_data = m.group(1)
self.last_xref_section_offset = int(m.group(2))
self.trailer_dict = self.interpret_trailer(trailer_data)
@ -644,12 +683,14 @@ class PdfParser:
if b"Prev" in self.trailer_dict:
self.read_prev_trailer(self.trailer_dict[b"Prev"])
def read_prev_trailer(self, xref_section_offset):
def read_prev_trailer(self, xref_section_offset: int) -> None:
assert self.buf is not None
trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
m = self.re_trailer_prev.search(
self.buf[trailer_offset : trailer_offset + 16384]
)
check_format_condition(m, "previous trailer not found")
check_format_condition(m is not None, "previous trailer not found")
assert m is not None
trailer_data = m.group(1)
check_format_condition(
int(m.group(2)) == xref_section_offset,
@ -670,7 +711,7 @@ class PdfParser:
re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional)
@classmethod
def interpret_trailer(cls, trailer_data):
def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]:
trailer = {}
offset = 0
while True:
@ -678,14 +719,18 @@ class PdfParser:
if not m:
m = cls.re_dict_end.match(trailer_data, offset)
check_format_condition(
m and m.end() == len(trailer_data),
m is not None and m.end() == len(trailer_data),
"name not found in trailer, remaining data: "
+ repr(trailer_data[offset:]),
)
break
key = cls.interpret_name(m.group(1))
value, offset = cls.get_value(trailer_data, m.end())
assert isinstance(key, bytes)
value, value_offset = cls.get_value(trailer_data, m.end())
trailer[key] = value
if value_offset is None:
break
offset = value_offset
check_format_condition(
b"Size" in trailer and isinstance(trailer[b"Size"], int),
"/Size not in trailer or not an integer",
@ -699,7 +744,7 @@ class PdfParser:
re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
@classmethod
def interpret_name(cls, raw, as_text=False):
def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes:
name = b""
for m in cls.re_hashes_in_name.finditer(raw):
if m.group(3):
@ -761,7 +806,13 @@ class PdfParser:
)
@classmethod
def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
def get_value(
cls,
data: bytes | bytearray | mmap.mmap,
offset: int,
expect_indirect: IndirectReference | None = None,
max_nesting: int = -1,
) -> tuple[Any, int | None]:
if max_nesting == 0:
return None, None
m = cls.re_comment.match(data, offset)
@ -783,11 +834,16 @@ class PdfParser:
== IndirectReference(int(m.group(1)), int(m.group(2))),
"indirect object definition different than expected",
)
object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1)
if offset is None:
object, object_offset = cls.get_value(
data, m.end(), max_nesting=max_nesting - 1
)
if object_offset is None:
return object, None
m = cls.re_indirect_def_end.match(data, offset)
check_format_condition(m, "indirect object definition end not found")
m = cls.re_indirect_def_end.match(data, object_offset)
check_format_condition(
m is not None, "indirect object definition end not found"
)
assert m is not None
return object, m.end()
check_format_condition(
not expect_indirect, "indirect object definition not found"
@ -806,46 +862,53 @@ class PdfParser:
m = cls.re_dict_start.match(data, offset)
if m:
offset = m.end()
result = {}
result: dict[Any, Any] = {}
m = cls.re_dict_end.match(data, offset)
current_offset: int | None = offset
while not m:
key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
if offset is None:
assert current_offset is not None
key, current_offset = cls.get_value(
data, current_offset, max_nesting=max_nesting - 1
)
if current_offset is None:
return result, None
value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
value, current_offset = cls.get_value(
data, current_offset, max_nesting=max_nesting - 1
)
result[key] = value
if offset is None:
if current_offset is None:
return result, None
m = cls.re_dict_end.match(data, offset)
offset = m.end()
m = cls.re_stream_start.match(data, offset)
m = cls.re_dict_end.match(data, current_offset)
current_offset = m.end()
m = cls.re_stream_start.match(data, current_offset)
if m:
try:
stream_len_str = result.get(b"Length")
stream_len = int(stream_len_str)
except (TypeError, ValueError) as e:
msg = f"bad or missing Length in stream dict ({stream_len_str})"
raise PdfFormatError(msg) from e
stream_len = result.get(b"Length")
if stream_len is None or not isinstance(stream_len, int):
msg = f"bad or missing Length in stream dict ({stream_len})"
raise PdfFormatError(msg)
stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len)
check_format_condition(m, "stream end not found")
offset = m.end()
result = PdfStream(PdfDict(result), stream_data)
else:
result = PdfDict(result)
return result, offset
check_format_condition(m is not None, "stream end not found")
assert m is not None
current_offset = m.end()
return PdfStream(PdfDict(result), stream_data), current_offset
return PdfDict(result), current_offset
m = cls.re_array_start.match(data, offset)
if m:
offset = m.end()
result = []
results = []
m = cls.re_array_end.match(data, offset)
current_offset = offset
while not m:
value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
result.append(value)
if offset is None:
return result, None
m = cls.re_array_end.match(data, offset)
return result, m.end()
assert current_offset is not None
value, current_offset = cls.get_value(
data, current_offset, max_nesting=max_nesting - 1
)
results.append(value)
if current_offset is None:
return results, None
m = cls.re_array_end.match(data, current_offset)
return results, m.end()
m = cls.re_null.match(data, offset)
if m:
return None, m.end()
@ -905,7 +968,9 @@ class PdfParser:
}
@classmethod
def get_literal_string(cls, data, offset):
def get_literal_string(
cls, data: bytes | bytearray | mmap.mmap, offset: int
) -> tuple[bytes, int]:
nesting_depth = 0
result = bytearray()
for m in cls.re_lit_str_token.finditer(data, offset):
@ -941,12 +1006,14 @@ class PdfParser:
)
re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
def read_xref_table(self, xref_section_offset):
def read_xref_table(self, xref_section_offset: int) -> int:
assert self.buf is not None
subsection_found = False
m = self.re_xref_section_start.match(
self.buf, xref_section_offset + self.start_offset
)
check_format_condition(m, "xref section start not found")
check_format_condition(m is not None, "xref section start not found")
assert m is not None
offset = m.end()
while True:
m = self.re_xref_subsection_start.match(self.buf, offset)
@ -961,7 +1028,8 @@ class PdfParser:
num_objects = int(m.group(2))
for i in range(first_object, first_object + num_objects):
m = self.re_xref_entry.match(self.buf, offset)
check_format_condition(m, "xref entry not found")
check_format_condition(m is not None, "xref entry not found")
assert m is not None
offset = m.end()
is_free = m.group(3) == b"f"
if not is_free:
@ -971,13 +1039,14 @@ class PdfParser:
self.xref_table[i] = new_entry
return offset
def read_indirect(self, ref, max_nesting=-1):
def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any:
offset, generation = self.xref_table[ref[0]]
check_format_condition(
generation == ref[1],
f"expected to find generation {ref[1]} for object ID {ref[0]} in xref "
f"table, instead found generation {generation} at offset {offset}",
)
assert self.buf is not None
value = self.get_value(
self.buf,
offset + self.start_offset,
@ -987,14 +1056,15 @@ class PdfParser:
self.cached_objects[ref] = value
return value
def linearize_page_tree(self, node=None):
if node is None:
node = self.page_tree_root
def linearize_page_tree(
self, node: PdfDict | None = None
) -> list[IndirectReference]:
page_node = node if node is not None else self.page_tree_root
check_format_condition(
node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
)
pages = []
for kid in node[b"Kids"]:
for kid in page_node[b"Kids"]:
kid_object = self.read_indirect(kid)
if kid_object[b"Type"] == b"Page":
pages.append(kid)

View File

@ -39,7 +39,7 @@ import struct
import warnings
import zlib
from enum import IntEnum
from typing import IO, TYPE_CHECKING, Any, NoReturn
from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
@ -144,7 +144,7 @@ def _safe_zlib_decompress(s):
return plaintext
def _crc32(data, seed=0):
def _crc32(data: bytes, seed: int = 0) -> int:
return zlib.crc32(data, seed) & 0xFFFFFFFF
@ -191,7 +191,7 @@ class ChunkStream:
assert self.queue is not None
self.queue.append((cid, pos, length))
def call(self, cid, pos, length):
def call(self, cid: bytes, pos: int, length: int) -> bytes:
"""Call the appropriate chunk handler"""
logger.debug("STREAM %r %s %s", cid, pos, length)
@ -230,6 +230,7 @@ class ChunkStream:
cids = []
assert self.fp is not None
while True:
try:
cid, pos, length = self.read()
@ -407,6 +408,7 @@ class PngStream(ChunkStream):
def chunk_iCCP(self, pos: int, length: int) -> bytes:
# ICC profile
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
# according to PNG spec, the iCCP chunk contains:
# Profile name 1-79 bytes (character string)
@ -434,6 +436,7 @@ class PngStream(ChunkStream):
def chunk_IHDR(self, pos: int, length: int) -> bytes:
# image header
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 13:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -471,6 +474,7 @@ class PngStream(ChunkStream):
def chunk_PLTE(self, pos: int, length: int) -> bytes:
# palette
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
self.im_palette = "RGB", s
@ -478,6 +482,7 @@ class PngStream(ChunkStream):
def chunk_tRNS(self, pos: int, length: int) -> bytes:
# transparency
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P":
if _simple_palette.match(s):
@ -498,6 +503,7 @@ class PngStream(ChunkStream):
def chunk_gAMA(self, pos: int, length: int) -> bytes:
# gamma setting
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
self.im_info["gamma"] = i32(s) / 100000.0
return s
@ -506,6 +512,7 @@ class PngStream(ChunkStream):
# chromaticity, 8 unsigned ints, actual value is scaled by 100,000
# WP x,y, Red x,y, Green x,y Blue x,y
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
raw_vals = struct.unpack(">%dI" % (len(s) // 4), s)
self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
@ -518,6 +525,7 @@ class PngStream(ChunkStream):
# 2 saturation
# 3 absolute colorimetric
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 1:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -529,6 +537,7 @@ class PngStream(ChunkStream):
def chunk_pHYs(self, pos: int, length: int) -> bytes:
# pixels per unit
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 9:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -546,6 +555,7 @@ class PngStream(ChunkStream):
def chunk_tEXt(self, pos: int, length: int) -> bytes:
# text
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
try:
k, v = s.split(b"\0", 1)
@ -554,17 +564,18 @@ class PngStream(ChunkStream):
k = s
v = b""
if k:
k = k.decode("latin-1", "strict")
k_str = k.decode("latin-1", "strict")
v_str = v.decode("latin-1", "replace")
self.im_info[k] = v if k == "exif" else v_str
self.im_text[k] = v_str
self.im_info[k_str] = v if k == b"exif" else v_str
self.im_text[k_str] = v_str
self.check_text_memory(len(v_str))
return s
def chunk_zTXt(self, pos: int, length: int) -> bytes:
# compressed text
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
try:
k, v = s.split(b"\0", 1)
@ -589,16 +600,17 @@ class PngStream(ChunkStream):
v = b""
if k:
k = k.decode("latin-1", "strict")
v = v.decode("latin-1", "replace")
k_str = k.decode("latin-1", "strict")
v_str = v.decode("latin-1", "replace")
self.im_info[k] = self.im_text[k] = v
self.check_text_memory(len(v))
self.im_info[k_str] = self.im_text[k_str] = v_str
self.check_text_memory(len(v_str))
return s
def chunk_iTXt(self, pos: int, length: int) -> bytes:
# international text
assert self.fp is not None
r = s = ImageFile._safe_read(self.fp, length)
try:
k, r = r.split(b"\0", 1)
@ -627,25 +639,27 @@ class PngStream(ChunkStream):
if k == b"XML:com.adobe.xmp":
self.im_info["xmp"] = v
try:
k = k.decode("latin-1", "strict")
lang = lang.decode("utf-8", "strict")
tk = tk.decode("utf-8", "strict")
v = v.decode("utf-8", "strict")
k_str = k.decode("latin-1", "strict")
lang_str = lang.decode("utf-8", "strict")
tk_str = tk.decode("utf-8", "strict")
v_str = v.decode("utf-8", "strict")
except UnicodeError:
return s
self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
self.check_text_memory(len(v))
self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str)
self.check_text_memory(len(v_str))
return s
def chunk_eXIf(self, pos: int, length: int) -> bytes:
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
self.im_info["exif"] = b"Exif\x00\x00" + s
return s
# APNG chunks
def chunk_acTL(self, pos: int, length: int) -> bytes:
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -666,6 +680,7 @@ class PngStream(ChunkStream):
return s
def chunk_fcTL(self, pos: int, length: int) -> bytes:
assert self.fp is not None
s = ImageFile._safe_read(self.fp, length)
if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -695,6 +710,7 @@ class PngStream(ChunkStream):
return s
def chunk_fdAT(self, pos: int, length: int) -> bytes:
assert self.fp is not None
if length < 4:
if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length)
@ -1075,21 +1091,21 @@ _OUTMODES = {
}
def putchunk(fp, cid, *data):
def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
"""Write a PNG chunk (including CRC field)"""
data = b"".join(data)
byte_data = b"".join(data)
fp.write(o32(len(data)) + cid)
fp.write(data)
crc = _crc32(data, _crc32(cid))
fp.write(o32(len(byte_data)) + cid)
fp.write(byte_data)
crc = _crc32(byte_data, _crc32(cid))
fp.write(o32(crc))
class _idat:
# wrap output from the encoder in IDAT chunks
def __init__(self, fp, chunk):
def __init__(self, fp, chunk) -> None:
self.fp = fp
self.chunk = chunk
@ -1100,7 +1116,7 @@ class _idat:
class _fdat:
# wrap encoder output in fdAT chunks
def __init__(self, fp, chunk, seq_num):
def __init__(self, fp: IO[bytes], chunk, seq_num: int) -> None:
self.fp = fp
self.chunk = chunk
self.seq_num = seq_num
@ -1110,7 +1126,21 @@ class _fdat:
self.seq_num += 1
def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images):
class _Frame(NamedTuple):
im: Image.Image
bbox: tuple[int, int, int, int] | None
encoderinfo: dict[str, Any]
def _write_multiple_frames(
im: Image.Image,
fp: IO[bytes],
chunk,
mode: str,
rawmode: str,
default_image: Image.Image | None,
append_images: list[Image.Image],
) -> Image.Image | None:
duration = im.encoderinfo.get("duration")
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
@ -1126,7 +1156,7 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
for imSequence in imSequences:
total += getattr(imSequence, "n_frames", 1)
im_frames = []
im_frames: list[_Frame] = []
frame_count = 0
for i, imSequence in enumerate(imSequences):
for im_frame in ImageSequence.Iterator(imSequence):
@ -1147,24 +1177,24 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
if im_frames:
previous = im_frames[-1]
prev_disposal = previous["encoderinfo"].get("disposal")
prev_blend = previous["encoderinfo"].get("blend")
prev_disposal = previous.encoderinfo.get("disposal")
prev_blend = previous.encoderinfo.get("blend")
if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
prev_disposal = Disposal.OP_BACKGROUND
if prev_disposal == Disposal.OP_BACKGROUND:
base_im = previous["im"].copy()
base_im = previous.im.copy()
dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
bbox = previous["bbox"]
bbox = previous.bbox
if bbox:
dispose = dispose.crop(bbox)
else:
bbox = (0, 0) + im.size
base_im.paste(dispose, bbox)
elif prev_disposal == Disposal.OP_PREVIOUS:
base_im = im_frames[-2]["im"]
base_im = im_frames[-2].im
else:
base_im = previous["im"]
base_im = previous.im
delta = ImageChops.subtract_modulo(
im_frame.convert("RGBA"), base_im.convert("RGBA")
)
@ -1175,18 +1205,18 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
and prev_blend == encoderinfo.get("blend")
and "duration" in encoderinfo
):
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
previous.encoderinfo["duration"] += encoderinfo["duration"]
if progress:
im._save_all_progress(imSequence, i, frame_count, total)
continue
else:
bbox = None
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
im_frames.append(_Frame(im_frame, bbox, encoderinfo))
if progress:
im._save_all_progress(imSequence, i, frame_count, total)
if len(im_frames) == 1 and not default_image:
return im_frames[0]["im"]
return im_frames[0].im
# animation control
chunk(
@ -1204,14 +1234,14 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
seq_num = 0
for frame, frame_data in enumerate(im_frames):
im_frame = frame_data["im"]
if not frame_data["bbox"]:
im_frame = frame_data.im
if not frame_data.bbox:
bbox = (0, 0) + im_frame.size
else:
bbox = frame_data["bbox"]
bbox = frame_data.bbox
im_frame = im_frame.crop(bbox)
size = im_frame.size
encoderinfo = frame_data["encoderinfo"]
encoderinfo = frame_data.encoderinfo
frame_duration = int(round(encoderinfo.get("duration", 0)))
frame_disposal = encoderinfo.get("disposal", disposal)
frame_blend = encoderinfo.get("blend", blend)
@ -1246,13 +1276,16 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
[("zip", (0, 0) + im_frame.size, 0, rawmode)],
)
seq_num = fdat_chunks.seq_num
return None
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True)
def _save(im, fp, filename, chunk=putchunk, save_all=False):
def _save(
im: Image.Image, fp, filename: str | bytes, chunk=putchunk, save_all: bool = False
) -> None:
# save an image to disk (called by the save method)
if save_all:
@ -1428,12 +1461,15 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
exif = exif[6:]
chunk(fp, b"eXIf", exif)
single_im: Image.Image | None = im
if save_all:
im = _write_multiple_frames(
single_im = _write_multiple_frames(
im, fp, chunk, mode, rawmode, default_image, append_images
)
if im:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
if single_im:
ImageFile._save(
single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)]
)
if info:
for info_chunk in info.chunks:
@ -1454,7 +1490,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
# PNG chunk converter
def getchunks(im, **params):
def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]:
"""Return a list of PNG chunks representing this image."""
class collector:
@ -1463,19 +1499,19 @@ def getchunks(im, **params):
def write(self, data: bytes) -> None:
pass
def append(self, chunk: bytes) -> None:
def append(self, chunk: tuple[bytes, bytes, bytes]) -> None:
self.data.append(chunk)
def append(fp, cid, *data):
data = b"".join(data)
crc = o32(_crc32(data, _crc32(cid)))
fp.append((cid, data, crc))
def append(fp: collector, cid: bytes, *data: bytes) -> None:
byte_data = b"".join(data)
crc = o32(_crc32(byte_data, _crc32(cid)))
fp.append((cid, byte_data, crc))
fp = collector()
try:
im.encoderinfo = params
_save(im, fp, None, append)
_save(im, fp, "", append)
finally:
del im.encoderinfo

View File

@ -185,7 +185,7 @@ def _layerinfo(fp, ct_bytes):
# read layerinfo block
layers = []
def read(size):
def read(size: int) -> bytes:
return ImageFile._safe_read(fp, size)
ct = si16(read(2))

View File

@ -334,12 +334,13 @@ class IFDRational(Rational):
__slots__ = ("_numerator", "_denominator", "_val")
def __init__(self, value, denominator=1):
def __init__(self, value, denominator: int = 1) -> None:
"""
:param value: either an integer numerator, a
float/rational/other number, or an IFDRational
:param denominator: Optional integer denominator
"""
self._val: Fraction | float
if isinstance(value, IFDRational):
self._numerator = value.numerator
self._denominator = value.denominator
@ -636,13 +637,13 @@ class ImageFileDirectory_v2(_IFDv2Base):
val = (val,)
return val
def __contains__(self, tag):
def __contains__(self, tag: object) -> bool:
return tag in self._tags_v2 or tag in self._tagdata
def __setitem__(self, tag, value):
def __setitem__(self, tag, value) -> None:
self._setitem(tag, value, self.legacy_api)
def _setitem(self, tag, value, legacy_api):
def _setitem(self, tag, value, legacy_api) -> None:
basetypes = (Number, bytes, str)
info = TiffTags.lookup(tag, self.group)
@ -758,7 +759,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return data
@_register_writer(1) # Basic type, except for the legacy API.
def write_byte(self, data):
def write_byte(self, data) -> bytes:
if isinstance(data, IFDRational):
data = int(data)
if isinstance(data, int):
@ -772,7 +773,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return data.decode("latin-1", "replace")
@_register_writer(2)
def write_string(self, value):
def write_string(self, value) -> bytes:
# remerge of https://github.com/python-pillow/Pillow/pull/1416
if isinstance(value, int):
value = str(value)
@ -790,7 +791,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@_register_writer(5)
def write_rational(self, *values):
def write_rational(self, *values) -> bytes:
return b"".join(
self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
)
@ -800,7 +801,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return data
@_register_writer(7)
def write_undefined(self, value):
def write_undefined(self, value) -> bytes:
if isinstance(value, IFDRational):
value = int(value)
if isinstance(value, int):
@ -817,13 +818,13 @@ class ImageFileDirectory_v2(_IFDv2Base):
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@_register_writer(10)
def write_signed_rational(self, *values):
def write_signed_rational(self, *values) -> bytes:
return b"".join(
self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
for frac in values
)
def _ensure_read(self, fp, size):
def _ensure_read(self, fp: IO[bytes], size: int) -> bytes:
ret = fp.read(size)
if len(ret) != size:
msg = (
@ -977,7 +978,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
return result
def save(self, fp):
def save(self, fp: IO[bytes]) -> int:
if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8
fp.write(self._prefix + self._pack("HL", 42, 8))
@ -1017,7 +1018,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
.. deprecated:: 3.0.0
"""
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._legacy_api = True
@ -1029,7 +1030,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
"""Dictionary of tag types"""
@classmethod
def from_v2(cls, original):
def from_v2(cls, original) -> ImageFileDirectory_v1:
"""Returns an
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
instance with the same data as is contained in the original
@ -1063,7 +1064,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
ifd._tags_v2 = dict(self._tags_v2)
return ifd
def __contains__(self, tag):
def __contains__(self, tag: object) -> bool:
return tag in self._tags_v1 or tag in self._tagdata
def __len__(self) -> int:
@ -1072,7 +1073,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __iter__(self):
return iter(set(self._tagdata) | set(self._tags_v1))
def __setitem__(self, tag, value):
def __setitem__(self, tag, value) -> None:
for legacy_api in (False, True):
self._setitem(tag, value, legacy_api)
@ -1122,7 +1123,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.tag_v2 = ImageFileDirectory_v2(ifh)
# legacy IFD entries will be filled in later
self.ifd = None
self.ifd: ImageFileDirectory_v1 | None = None
# setup frame pointers
self.__first = self.__next = self.tag_v2.next
@ -1232,7 +1233,7 @@ class TiffImageFile(ImageFile.ImageFile):
val = val[math.ceil((10 + n + size) / 2) * 2 :]
return blocks
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if self.tile and self.use_load_libtiff:
return self._load_libtiff()
return super().load()
@ -1343,7 +1344,7 @@ class TiffImageFile(ImageFile.ImageFile):
return Image.Image.load(self)
def _setup(self):
def _setup(self) -> None:
"""Setup this image object based on current tags"""
if 0xBC01 in self.tag_v2:
@ -1537,13 +1538,13 @@ class TiffImageFile(ImageFile.ImageFile):
# adjust stride width accordingly
stride /= bps_count
a = (tile_rawmode, int(stride), 1)
args = (tile_rawmode, int(stride), 1)
self.tile.append(
(
self._compression,
(x, y, min(x + w, xsize), min(y + h, ysize)),
offset,
a,
args,
)
)
x = x + w
@ -1938,7 +1939,7 @@ class AppendingTiffWriter:
521, # JPEGACTables
}
def __init__(self, fn, new=False):
def __init__(self, fn, new: bool = False) -> None:
if hasattr(fn, "read"):
self.f = fn
self.close_fp = False
@ -2015,7 +2016,7 @@ class AppendingTiffWriter:
def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage
def seek(self, offset, whence=io.SEEK_SET):
def seek(self, offset: int, whence=io.SEEK_SET) -> int:
if whence == os.SEEK_SET:
offset += self.offsetOfNewPage

View File

@ -24,8 +24,11 @@ and has been tested with a few sample files found using google.
"""
from __future__ import annotations
from typing import IO
from . import Image, ImageFile
from ._binary import i32le as i32
from ._typing import StrOrBytesPath
class WalImageFile(ImageFile.ImageFile):
@ -50,7 +53,7 @@ class WalImageFile(ImageFile.ImageFile):
if next_name:
self.info["next_name"] = next_name
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if not self.im:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self.size[0] * self.size[1]))
@ -58,7 +61,7 @@ class WalImageFile(ImageFile.ImageFile):
return Image.Image.load(self)
def open(filename):
def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile:
"""
Load texture from a Quake2 WAL texture file.

View File

@ -115,7 +115,7 @@ class WebPImageFile(ImageFile.ImageFile):
self.__loaded = -1
self.__timestamp = 0
def _get_next(self):
def _get_next(self) -> tuple[bytes, int, int]:
# Get next frame
ret = self._decoder.get_next()
self.__physical_frame += 1
@ -144,7 +144,7 @@ class WebPImageFile(ImageFile.ImageFile):
while self.__physical_frame < frame:
self._get_next() # Advance to the requested frame
def load(self):
def load(self) -> Image.core.PixelAccess | None:
if _webp.HAVE_WEBPANIM:
if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame)

View File

@ -152,7 +152,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _load(self) -> ImageFile.StubHandler | None:
return _handler
def load(self, dpi=None):
def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None:
if dpi is not None and self._inch is not None:
self.info["dpi"] = dpi
x0, y0, x1, y1 = self.info["wmf_bbox"]

3
src/PIL/_imagingtk.pyi Normal file
View File

@ -0,0 +1,3 @@
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -2251,6 +2251,11 @@ _getcolors(ImagingObject *self, PyObject *args) {
ImagingColorItem *v = &items[i];
PyObject *item = Py_BuildValue(
"iN", v->count, getpixel(self->image, self->access, v->x, v->y));
if (item == NULL) {
Py_DECREF(out);
free(items);
return NULL;
}
PyList_SetItem(out, i, item);
}
}
@ -4448,5 +4453,9 @@ PyInit__imaging(void) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}

View File

@ -1538,5 +1538,9 @@ PyInit__imagingcms(void) {
PyDateTime_IMPORT;
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}

View File

@ -20,6 +20,7 @@
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include "thirdparty/pythoncapi_compat.h"
#include "libImaging/Imaging.h"
#include <ft2build.h>
@ -1209,30 +1210,49 @@ font_getvarnames(FontObject *self) {
return NULL;
}
int *list_names_filled = PyMem_Malloc(num_namedstyles * sizeof(int));
if (list_names_filled == NULL) {
Py_DECREF(list_names);
FT_Done_MM_Var(library, master);
return PyErr_NoMemory();
}
for (int i = 0; i < num_namedstyles; i++) {
list_names_filled[i] = 0;
}
name_count = FT_Get_Sfnt_Name_Count(self->face);
for (i = 0; i < name_count; i++) {
error = FT_Get_Sfnt_Name(self->face, i, &name);
if (error) {
PyMem_Free(list_names_filled);
Py_DECREF(list_names);
FT_Done_MM_Var(library, master);
return geterror(error);
}
for (j = 0; j < num_namedstyles; j++) {
if (PyList_GetItem(list_names, j) != NULL) {
if (list_names_filled[j]) {
continue;
}
if (master->namedstyle[j].strid == name.name_id) {
list_name = Py_BuildValue("y#", name.string, name.string_len);
if (list_name == NULL) {
PyMem_Free(list_names_filled);
Py_DECREF(list_names);
FT_Done_MM_Var(library, master);
return NULL;
}
PyList_SetItem(list_names, j, list_name);
list_names_filled[j] = 1;
break;
}
}
}
PyMem_Free(list_names_filled);
FT_Done_MM_Var(library, master);
return list_names;
}
@ -1289,9 +1309,14 @@ font_getvaraxes(FontObject *self) {
if (name.name_id == axis.strid) {
axis_name = Py_BuildValue("y#", name.string, name.string_len);
PyDict_SetItemString(
list_axis, "name", axis_name ? axis_name : Py_None);
Py_XDECREF(axis_name);
if (axis_name == NULL) {
Py_DECREF(list_axis);
Py_DECREF(list_axes);
FT_Done_MM_Var(library, master);
return NULL;
}
PyDict_SetItemString(list_axis, "name", axis_name);
Py_DECREF(axis_name);
break;
}
}
@ -1345,7 +1370,12 @@ font_setvaraxes(FontObject *self, PyObject *args) {
return PyErr_NoMemory();
}
for (i = 0; i < num_coords; i++) {
item = PyList_GET_ITEM(axes, i);
item = PyList_GetItemRef(axes, i);
if (item == NULL) {
free(coords);
return NULL;
}
if (PyFloat_Check(item)) {
coord = PyFloat_AS_DOUBLE(item);
} else if (PyLong_Check(item)) {
@ -1353,10 +1383,12 @@ font_setvaraxes(FontObject *self, PyObject *args) {
} else if (PyNumber_Check(item)) {
coord = PyFloat_AsDouble(item);
} else {
Py_DECREF(item);
free(coords);
PyErr_SetString(PyExc_TypeError, "list must contain numbers");
return NULL;
}
Py_DECREF(item);
coords[i] = coord * 65536;
}
@ -1576,5 +1608,9 @@ PyInit__imagingft(void) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}

View File

@ -290,5 +290,9 @@ PyInit__imagingmath(void) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}

View File

@ -269,5 +269,9 @@ PyInit__imagingmorph(void) {
m = PyModule_Create(&module_def);
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}

View File

@ -62,5 +62,10 @@ PyInit__imagingtk(void) {
Py_DECREF(m);
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}

View File

@ -1005,5 +1005,9 @@ PyInit__webp(void) {
return NULL;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
return m;
}

View File

@ -25,6 +25,7 @@
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include "thirdparty/pythoncapi_compat.h"
#include "libImaging/Imaging.h"
#include "libImaging/Gif.h"
@ -671,11 +672,17 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
tags_size = PyList_Size(tags);
TRACE(("tags size: %d\n", (int)tags_size));
for (pos = 0; pos < tags_size; pos++) {
item = PyList_GetItem(tags, pos);
item = PyList_GetItemRef(tags, pos);
if (item == NULL) {
return NULL;
}
if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) {
Py_DECREF(item);
PyErr_SetString(PyExc_ValueError, "Invalid tags list");
return NULL;
}
Py_DECREF(item);
}
pos = 0;
}
@ -703,11 +710,17 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
num_core_tags = sizeof(core_tags) / sizeof(int);
for (pos = 0; pos < tags_size; pos++) {
item = PyList_GetItem(tags, pos);
item = PyList_GetItemRef(tags, pos);
if (item == NULL) {
return NULL;
}
// We already checked that tags is a 2-tuple list.
key = PyTuple_GetItem(item, 0);
key = PyTuple_GET_ITEM(item, 0);
key_int = (int)PyLong_AsLong(key);
value = PyTuple_GetItem(item, 1);
value = PyTuple_GET_ITEM(item, 1);
Py_DECREF(item);
status = 0;
is_core_tag = 0;
is_var_length = 0;
@ -721,7 +734,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
}
if (!is_core_tag) {
PyObject *tag_type = PyDict_GetItem(types, key);
PyObject *tag_type;
if (PyDict_GetItemRef(types, key, &tag_type) < 0) {
return NULL; // Exception has been already set
}
if (tag_type) {
int type_int = PyLong_AsLong(tag_type);
if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) {

View File

@ -26,6 +26,7 @@
*/
#include "Python.h"
#include "thirdparty/pythoncapi_compat.h"
#include "libImaging/Imaging.h"
#include <math.h>
@ -179,14 +180,21 @@ PyPath_Flatten(PyObject *data, double **pxy) {
} \
free(xy); \
return -1; \
} \
if (decref) { \
Py_DECREF(op); \
}
/* Copy table to path array */
if (PyList_Check(data)) {
for (i = 0; i < n; i++) {
double x, y;
PyObject *op = PyList_GET_ITEM(data, i);
assign_item_to_array(op, 0);
PyObject *op = PyList_GetItemRef(data, i);
if (op == NULL) {
free(xy);
return -1;
}
assign_item_to_array(op, 1);
}
} else if (PyTuple_Check(data)) {
for (i = 0; i < n; i++) {
@ -209,7 +217,6 @@ PyPath_Flatten(PyObject *data, double **pxy) {
}
}
assign_item_to_array(op, 1);
Py_DECREF(op);
}
}

1360
src/thirdparty/pythoncapi_compat.h vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -38,9 +38,11 @@ deps =
ipython
numpy
packaging
pytest
types-defusedxml
types-olefile
types-setuptools
extras =
typing
commands =
mypy src {posargs}
mypy src Tests {posargs}