mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-18 20:41:02 +03:00
Merge branch 'main' into progress
This commit is contained in:
commit
8b4b7ce7dd
|
@ -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
|
||||
|
|
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
|
@ -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
|
||||
|
||||
|
|
6
.github/workflows/wheels-test.sh
vendored
6
.github/workflows/wheels-test.sh
vendored
|
@ -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
|
||||
|
|
9
.github/workflows/wheels.yml
vendored
9
.github/workflows/wheels.yml
vendored
|
@ -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
|
||||
|
|
15
CHANGES.rst
15
CHANGES.rst
|
@ -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)
|
||||
-------------------
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
| +----------------------------+------------------+ |
|
||||
|
|
|
@ -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$',
|
||||
]
|
||||
|
|
|
@ -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]]:
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
3
src/PIL/_imagingtk.pyi
Normal file
|
@ -0,0 +1,3 @@
|
|||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
26
src/encode.c
26
src/encode.c
|
@ -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) {
|
||||
|
|
13
src/path.c
13
src/path.c
|
@ -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
1360
src/thirdparty/pythoncapi_compat.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user