Merge branch 'main' into init

This commit is contained in:
Andrew Murray 2024-08-25 00:05:56 +10:00 committed by GitHub
commit 73d2bc3b75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 1035 additions and 1000 deletions

View File

@ -51,7 +51,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov

View File

@ -1 +1 @@
cibuildwheel==2.19.2 cibuildwheel==2.20.0

View File

@ -1 +1,11 @@
mypy==1.11.0 mypy==1.11.1
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pytest
sphinx
types-defusedxml
types-olefile
types-setuptools

View File

@ -2,6 +2,9 @@
set -e set -e
if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \ brew install \
freetype \ freetype \
ghostscript \ ghostscript \
@ -20,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov

View File

@ -74,6 +74,7 @@ jobs:
perl perl
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter python3${{ matrix.python-minor-version }}-tkinter

View File

@ -1,12 +1,12 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0 rev: v0.6.0
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2 rev: 24.8.0
hooks: hooks:
- id: black - id: black
@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.6 rev: 0.29.1
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
@ -62,12 +62,12 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.1.3 rev: 2.2.1
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.18 rev: v0.19
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

View File

@ -5,6 +5,33 @@ Changelog (Pillow)
11.0.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- Updated error message when saving WebP with invalid width or height #8322
[radarhere, hugovk]
- Remove warning if NumPy failed to raise an error during conversion #8326
[radarhere]
- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304
[radarhere]
- Remove WebP support without anim, mux/demux, and with buggy alpha #8213
[homm, radarhere]
- Add missing TIFF CMYK;16B reader #8298
[homm]
- Remove all WITH_* flags from _imaging.c and other flags #8211
[homm]
- Improve ImageDraw2 shape methods #8265
[radarhere]
- Lock around usages of imaging memory arenas #8238
[lysnikolaou]
- Deprecate JpegImageFile huffman_ac and huffman_dc #8274
[radarhere]
- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242 - Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242
[radarhere] [radarhere]

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

View File

@ -105,91 +105,68 @@ class TestColorLut3DCoreAPI:
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
def test_correct_args(self) -> None: @pytest.mark.parametrize(
"lut_mode, table_channels, table_size",
[
("RGB", 3, 3),
("CMYK", 4, 3),
("RGB", 3, (2, 3, 3)),
("RGB", 3, (65, 3, 3)),
("RGB", 3, (3, 65, 3)),
("RGB", 3, (2, 3, 65)),
],
)
def test_correct_args(
self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int]
) -> None:
im = Image.new("RGB", (10, 10), 0) im = Image.new("RGB", (10, 10), 0)
assert im.im is not None
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) lut_mode,
)
im.im.color_lut_3d(
"CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 3, 3)), *self.generate_identity_table(table_channels, table_size),
) )
@pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("L", "RGB", 3, 3),
("RGB", "L", 3, 3),
("L", "L", 3, 3),
("RGB", "RGBA", 3, 3),
("RGB", "RGB", 4, 3),
],
)
def test_wrong_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new(image_mode, (10, 10), 0)
assert im.im is not None
im.im.color_lut_3d(
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(table_channels, table_size),
)
@pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("RGBA", "RGBA", 3, 3),
("RGBA", "RGBA", 4, 3),
("RGB", "HSV", 3, 3),
("RGB", "RGBA", 4, 3),
],
)
def test_correct_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
im = Image.new(image_mode, (10, 10), 0)
assert im.im is not None
im.im.color_lut_3d( im.im.color_lut_3d(
"RGB", lut_mode,
Image.Resampling.BILINEAR, Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (65, 3, 3)), *self.generate_identity_table(table_channels, table_size),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 65, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 3, 65)),
)
def test_wrong_mode(self) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_correct_mode(self) -> None:
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
) )
def test_identities(self) -> None: def test_identities(self) -> None:

View File

@ -10,11 +10,6 @@ from PIL import features
from .helper import skip_unless_feature from .helper import skip_unless_feature
try:
from PIL import _webp
except ImportError:
pass
def test_check() -> None: def test_check() -> None:
# Check the correctness of the convenience function # Check the correctness of the convenience function
@ -23,7 +18,11 @@ def test_check() -> None:
for codec in features.codecs: for codec in features.codecs:
assert features.check_codec(codec) == features.check(codec) assert features.check_codec(codec) == features.check(codec)
for feature in features.features: for feature in features.features:
assert features.check_feature(feature) == features.check(feature) if "webp" in feature:
with pytest.warns(DeprecationWarning):
assert features.check_feature(feature) == features.check(feature)
else:
assert features.check_feature(feature) == features.check(feature)
def test_version() -> None: def test_version() -> None:
@ -48,23 +47,26 @@ def test_version() -> None:
for codec in features.codecs: for codec in features.codecs:
test(codec, features.version_codec) test(codec, features.version_codec)
for feature in features.features: for feature in features.features:
test(feature, features.version_feature) if "webp" in feature:
with pytest.warns(DeprecationWarning):
test(feature, features.version_feature)
else:
test(feature, features.version_feature)
@skip_unless_feature("webp")
def test_webp_transparency() -> None: def test_webp_transparency() -> None:
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() with pytest.warns(DeprecationWarning):
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY assert features.check("transp_webp") == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_mux() -> None: def test_webp_mux() -> None:
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX with pytest.warns(DeprecationWarning):
assert features.check("webp_mux") == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_anim() -> None: def test_webp_anim() -> None:
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM with pytest.warns(DeprecationWarning):
assert features.check("webp_anim") == features.check_module("webp")
@skip_unless_feature("libjpeg_turbo") @skip_unless_feature("libjpeg_turbo")

View File

@ -152,7 +152,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("image_path", "expected_path"), "image_path, expected_path",
( (
# hexeditted to be typeless # hexeditted to be typeless
(TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM),
@ -248,7 +248,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "size", "test_file"), "mode, size, test_file",
[ [
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
@ -373,7 +373,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "test_file"), "mode, test_file",
[ [
("L", "Tests/images/linear_gradient.png"), ("L", "Tests/images/linear_gradient.png"),
("LA", "Tests/images/uncompressed_la.png"), ("LA", "Tests/images/uncompressed_la.png"),

View File

@ -80,9 +80,7 @@ simple_eps_file_with_long_binary_data = (
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize( @pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252))))
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
)
@pytest.mark.parametrize("scale", (1, 2)) @pytest.mark.parametrize("scale", (1, 2))
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size) expected_size = tuple(s * scale for s in size)

View File

@ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
# Test opaque WebP background # Test opaque WebP background
if features.check("webp") and features.check("webp_anim"): if features.check("webp"):
with Image.open("Tests/images/hopper.webp") as im: with Image.open("Tests/images/hopper.webp") as im:
assert im.info["background"] == (255, 255, 255, 255) assert im.info["background"] == (255, 255, 255, 255)
im.save(out) im.save(out)

View File

@ -77,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None:
assert len(iptc) == 3 assert len(iptc) == 3
def test_getiptcinfo_tiff() -> None:
# Arrange
with Image.open("Tests/images/hopper.Lab.tif") as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"}
def test_getiptcinfo_tiff_none() -> None: def test_getiptcinfo_tiff_none() -> None:
# Arrange # Arrange
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:

View File

@ -154,7 +154,7 @@ class TestFileJpeg:
assert k > 0.9 assert k > 0.9
def test_rgb(self) -> None: def test_rgb(self) -> None:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
return tuple(v[0] for v in im.layer) return tuple(v[0] for v in im.layer)
im = hopper() im = hopper()
@ -1019,13 +1019,16 @@ class TestFileJpeg:
# SOI, EOI # SOI, EOI
for marker in b"\xff\xd8", b"\xff\xd9": for marker in b"\xff\xd8", b"\xff\xd9":
assert marker in data[1] and marker in data[2] assert marker in data[1]
assert marker in data[2]
# DHT, DQT # DHT, DQT
for marker in b"\xff\xc4", b"\xff\xdb": for marker in b"\xff\xc4", b"\xff\xdb":
assert marker in data[1] and marker not in data[2] assert marker in data[1]
assert marker not in data[2]
# SOF0, SOS, APP0 (JFIF header) # SOF0, SOS, APP0 (JFIF header)
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
assert marker not in data[1] and marker in data[2] assert marker not in data[1]
assert marker in data[2]
with Image.open(BytesIO(data[0])) as interchange_im: with Image.open(BytesIO(data[0])) as interchange_im:
with Image.open(BytesIO(data[1] + data[2])) as combined_im: with Image.open(BytesIO(data[1] + data[2])) as combined_im:
@ -1045,6 +1048,13 @@ class TestFileJpeg:
assert im._repr_jpeg_() is None assert im._repr_jpeg_() is None
def test_deprecation(self) -> None:
with Image.open(TEST_FILE) as im:
with pytest.warns(DeprecationWarning):
assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning):
assert im.huffman_dc == {}
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg") @skip_unless_feature("jpg")

View File

@ -233,7 +233,7 @@ def test_layers() -> None:
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"), ("foo.jp2", {"no_jp2": False}, 4, b"jP"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"), (None, {"no_jp2": False}, 4, b"jP"),
), ),
) )
def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None:

View File

@ -684,6 +684,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile) assert_image_equal_tofile(reloaded, infile)
def test_invalid_tiled_dimensions(self) -> None:
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
data = fp.read()
b = BytesIO(data[:144] + b"\x02" + data[145:])
with pytest.raises(ValueError):
Image.open(b)
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None: def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")

View File

@ -48,8 +48,6 @@ class TestFileWebp:
self.rgb_mode = "RGB" self.rgb_mode = "RGB"
def test_version(self) -> None: def test_version(self) -> None:
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
version = features.version_module("webp") version = features.version_module("webp")
assert version is not None assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version) assert re.search(r"\d+\.\d+\.\d+$", version)
@ -117,7 +115,6 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6) hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer() assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@skip_unless_feature("webp_anim")
def test_save_all(self, tmp_path: Path) -> None: def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))
@ -132,10 +129,9 @@ class TestFileWebp:
def test_icc_profile(self, tmp_path: Path) -> None: def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM: self._roundtrip(
self._roundtrip( tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} )
)
def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: def test_write_unsupported_mode_L(self, tmp_path: Path) -> None:
""" """
@ -161,27 +157,32 @@ class TestFileWebp:
im.save(temp_file, method=0) im.save(temp_file, method=0)
assert str(e.value) == "encoding error 6" assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("L", (16384, 16384))
with pytest.raises(ValueError) as e:
im.save(temp_file)
assert (
str(e.value)
== "encoding error 5: Image size exceeds WebP limit of 16383 pixels"
)
def test_WebPEncode_with_invalid_args(self) -> None: def test_WebPEncode_with_invalid_args(self) -> None:
""" """
Calling encoder functions with no arguments should result in an error. Calling encoder functions with no arguments should result in an error.
""" """
with pytest.raises(TypeError):
if _webp.HAVE_WEBPANIM: _webp.WebPAnimEncoder()
with pytest.raises(TypeError):
_webp.WebPAnimEncoder()
with pytest.raises(TypeError): with pytest.raises(TypeError):
_webp.WebPEncode() _webp.WebPEncode()
def test_WebPDecode_with_invalid_args(self) -> None: def test_WebPAnimDecoder_with_invalid_args(self) -> None:
""" """
Calling decoder functions with no arguments should result in an error. Calling decoder functions with no arguments should result in an error.
""" """
if _webp.HAVE_WEBPANIM:
with pytest.raises(TypeError):
_webp.WebPAnimDecoder()
with pytest.raises(TypeError): with pytest.raises(TypeError):
_webp.WebPDecode() _webp.WebPAnimDecoder()
def test_no_resource_warning(self, tmp_path: Path) -> None: def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp" file_path = "Tests/images/hopper.webp"
@ -200,7 +201,6 @@ class TestFileWebp:
"background", "background",
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
) )
@skip_unless_feature("webp_anim")
def test_invalid_background( def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path self, background: int | tuple[int, ...], tmp_path: Path
) -> None: ) -> None:
@ -209,7 +209,6 @@ class TestFileWebp:
with pytest.raises(OSError): with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background) im.save(temp_file, save_all=True, append_images=[im], background=background)
@skip_unless_feature("webp_anim")
def test_background_from_gif(self, tmp_path: Path) -> None: def test_background_from_gif(self, tmp_path: Path) -> None:
# Save L mode GIF with background # Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im: with Image.open("Tests/images/no_palette_with_background.gif") as im:
@ -234,7 +233,6 @@ class TestFileWebp:
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
assert difference < 5 assert difference < 5
@skip_unless_feature("webp_anim")
def test_duration(self, tmp_path: Path) -> None: def test_duration(self, tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im: with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000 assert im.info["duration"] == 1000

View File

@ -13,12 +13,7 @@ from .helper import (
hopper, hopper,
) )
_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") pytest.importorskip("PIL._webp", reason="WebP support not installed")
def setup_module() -> None:
if _webp.WebPDecoderBuggyAlpha():
pytest.skip("Buggy early version of WebP installed, not testing transparency")
def test_read_rgba() -> None: def test_read_rgba() -> None:
@ -81,9 +76,6 @@ def test_write_rgba(tmp_path: Path) -> None:
pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20))
pil_image.save(temp_file) pil_image.save(temp_file)
if _webp.WebPDecoderBuggyAlpha():
return
with Image.open(temp_file) as image: with Image.open(temp_file) as image:
image.load() image.load()
@ -93,12 +85,7 @@ def test_write_rgba(tmp_path: Path) -> None:
image.load() image.load()
image.getdata() image.getdata()
# Early versions of WebP are known to produce higher deviations: assert_image_similar(image, pil_image, 1.0)
# deal with it
if _webp.WebPDecoderVersion() <= 0x201:
assert_image_similar(image, pil_image, 3.0)
else:
assert_image_similar(image, pil_image, 1.0)
def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:

View File

@ -15,10 +15,7 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
pytestmark = [ pytestmark = skip_unless_feature("webp")
skip_unless_feature("webp"),
skip_unless_feature("webp_anim"),
]
def test_n_frames() -> None: def test_n_frames() -> None:

View File

@ -8,14 +8,11 @@ from PIL import Image
from .helper import assert_image_equal, hopper from .helper import assert_image_equal, hopper
_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") pytest.importorskip("PIL._webp", reason="WebP support not installed")
RGB_MODE = "RGB" RGB_MODE = "RGB"
def test_write_lossless_rgb(tmp_path: Path) -> None: def test_write_lossless_rgb(tmp_path: Path) -> None:
if _webp.WebPDecoderVersion() < 0x0200:
pytest.skip("lossless not included")
temp_file = str(tmp_path / "temp.webp") temp_file = str(tmp_path / "temp.webp")
hopper(RGB_MODE).save(temp_file, lossless=True) hopper(RGB_MODE).save(temp_file, lossless=True)

View File

@ -10,10 +10,7 @@ from PIL import Image
from .helper import mark_if_feature_version, skip_unless_feature from .helper import mark_if_feature_version, skip_unless_feature
pytestmark = [ pytestmark = skip_unless_feature("webp")
skip_unless_feature("webp"),
skip_unless_feature("webp_mux"),
]
ElementTree: ModuleType | None ElementTree: ModuleType | None
try: try:
@ -119,7 +116,15 @@ def test_read_no_exif() -> None:
def test_getxmp() -> None: def test_getxmp() -> None:
with Image.open("Tests/images/flower.webp") as im: with Image.open("Tests/images/flower.webp") as im:
assert "xmp" not in im.info assert "xmp" not in im.info
assert im.getxmp() == {} if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
with Image.open("Tests/images/flower2.webp") as im: with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None: if ElementTree is None:
@ -136,7 +141,6 @@ def test_getxmp() -> None:
) )
@skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path: Path) -> None: def test_write_animated_metadata(tmp_path: Path) -> None:
iccp_data = b"<iccp_data>" iccp_data = b"<iccp_data>"
exif_data = b"<exif_data>" exif_data = b"<exif_data>"

View File

@ -42,6 +42,12 @@ try:
except ImportError: except ImportError:
ElementTree = None ElementTree = None
PrettyPrinter: type | None
try:
from IPython.lib.pretty import PrettyPrinter
except ImportError:
PrettyPrinter = None
# Deprecation helper # Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -95,16 +101,15 @@ class TestImage:
with pytest.raises(TypeError): with pytest.raises(TypeError):
Image.Image() Image.Image()
@pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
def test_repr_pretty(self) -> None: def test_repr_pretty(self) -> None:
class Pretty:
def text(self, text: str) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100)) im = Image.new("L", (100, 100))
p = Pretty() output = io.StringIO()
im._repr_pretty_(p, None) assert PrettyPrinter is not None
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>" p = PrettyPrinter(output)
im._repr_pretty_(p, False)
assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"
def test_open_formats(self) -> None: def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png" PNGFILE = "Tests/images/hopper.png"
@ -821,7 +826,6 @@ class TestImage:
assert reloaded_exif[305] == "Pillow test" assert reloaded_exif[305] == "Pillow test"
@skip_unless_feature("webp") @skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_exif_webp(self, tmp_path: Path) -> None: def test_exif_webp(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.webp") as im: with Image.open("Tests/images/hopper.webp") as im:
exif = im.getexif() exif = im.getexif()
@ -943,7 +947,15 @@ class TestImage:
def test_empty_xmp(self) -> None: def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im: with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {} if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
def test_getxmp_padded(self) -> None: def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1)) im = Image.new("RGB", (1, 1))

View File

@ -230,7 +230,7 @@ class TestImagePutPixelError:
im.putpixel((0, 0), v) # type: ignore[arg-type] im.putpixel((0, 0), v) # type: ignore[arg-type]
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "band_numbers", "match"), "mode, band_numbers, match",
( (
("L", (0, 2), "color must be int or single-element tuple"), ("L", (0, 2), "color must be int or single-element tuple"),
("LA", (0, 3), "color must be int, or tuple of one or two elements"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"),

View File

@ -47,7 +47,7 @@ def test_toarray() -> None:
with pytest.raises(OSError): with pytest.raises(OSError):
numpy.array(im_truncated) numpy.array(im_truncated)
else: else:
with pytest.warns(UserWarning): with pytest.warns(DeprecationWarning):
numpy.array(im_truncated) numpy.array(im_truncated)

View File

@ -398,7 +398,8 @@ def test_logical() -> None:
for y in (a, b): for y in (a, b):
imy = Image.new("1", (1, 1), y) imy = Image.new("1", (1, 1), y)
value = op(imx, imy).getpixel((0, 0)) value = op(imx, imy).getpixel((0, 0))
assert not isinstance(value, tuple) and value is not None assert not isinstance(value, tuple)
assert value is not None
out.append(value) out.append(value)
return out return out

View File

@ -857,6 +857,27 @@ def test_rounded_rectangle_corners(
) )
def test_rounded_rectangle_joined_x_different_corners() -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA")
# Act
draw.rounded_rectangle(
(20, 10, 80, 90),
30,
fill="red",
outline="green",
width=5,
corners=(True, False, False, False),
)
# Assert
assert_image_equal_tofile(
im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"xy, radius, type", "xy, radius, type",
[ [

View File

@ -65,6 +65,36 @@ def test_mode() -> None:
ImageDraw2.Draw("L") ImageDraw2.Draw("L")
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("white", width=1)
# Act
draw.arc(bbox, pen, start, end)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
@pytest.mark.parametrize("bbox", BBOX)
def test_chord(bbox: Coords) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("yellow")
brush = ImageDraw2.Brush("red")
# Act
draw.chord(bbox, pen, 0, 180, brush)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_RGB.png", 1)
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(bbox: Coords) -> None: def test_ellipse(bbox: Coords) -> None:
# Arrange # Arrange
@ -123,6 +153,22 @@ def test_line_pen_as_brush(points: Coords) -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
@pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox: Coords, start: float, end: float) -> None:
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("blue")
brush = ImageDraw2.Brush("white")
# Act
draw.pieslice(bbox, pen, start, end, brush)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
@pytest.mark.parametrize("points", POINTS) @pytest.mark.parametrize("points", POINTS)
def test_polygon(points: Coords) -> None: def test_polygon(points: Coords) -> None:
# Arrange # Arrange

View File

@ -94,7 +94,6 @@ class TestImageFile:
assert (48, 48) == p.image.size assert (48, 48) == p.image.size
@skip_unless_feature("webp") @skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_incremental_webp(self) -> None: def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p: with ImageFile.Parser() as p:
with open("Tests/images/hopper.webp", "rb") as f: with open("Tests/images/hopper.webp", "rb") as f:
@ -318,7 +317,13 @@ class TestPyEncoder(CodecsTest):
fp = BytesIO() fp = BytesIO()
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] im,
fp,
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB"
)
],
) )
assert MockPyEncoder.last assert MockPyEncoder.last
@ -334,7 +339,7 @@ class TestPyEncoder(CodecsTest):
im.tile = [("MOCK", None, 32, None)] im.tile = [("MOCK", None, 32, None)]
fp = BytesIO() fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])
assert MockPyEncoder.last assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0 assert MockPyEncoder.last.state.xoff == 0
@ -351,7 +356,9 @@ class TestPyEncoder(CodecsTest):
MockPyEncoder.last = None MockPyEncoder.last = None
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")],
) )
last: MockPyEncoder | None = MockPyEncoder.last last: MockPyEncoder | None = MockPyEncoder.last
assert last assert last
@ -359,7 +366,9 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")],
) )
def test_oversize(self) -> None: def test_oversize(self) -> None:
@ -372,14 +381,22 @@ class TestPyEncoder(CodecsTest):
ImageFile._save( ImageFile._save(
im, im,
fp, fp,
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB"
)
],
) )
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, im,
fp, fp,
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB"
)
],
) )
def test_encode(self) -> None: def test_encode(self) -> None:
@ -395,9 +412,8 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd() encoder.encode_to_pyfd()
fh = BytesIO()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_file(fh, 0) encoder.encode_to_file(0, 0)
def test_zero_height(self) -> None: def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):

View File

@ -46,7 +46,8 @@ def img_to_string(im: Image.Image) -> str:
line = "" line = ""
for c in range(im.width): for c in range(im.width):
value = im.getpixel((c, r)) value = im.getpixel((c, r))
assert not isinstance(value, tuple) and value is not None assert not isinstance(value, tuple)
assert value is not None
line += chars[value > 0] line += chars[value > 0]
result.append(line) result.append(line)
return "\n".join(result) return "\n".join(result)

View File

@ -390,7 +390,7 @@ def test_colorize_3color_offset() -> None:
def test_exif_transpose() -> None: def test_exif_transpose() -> None:
exts = [".jpg"] exts = [".jpg"]
if features.check("webp") and features.check("webp_anim"): if features.check("webp"):
exts.append(".webp") exts.append(".webp")
for ext in exts: for ext in exts:
with Image.open("Tests/images/hopper" + ext) as base_im: with Image.open("Tests/images/hopper" + ext) as base_im:

View File

@ -46,7 +46,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
@pytest.mark.parametrize( @pytest.mark.parametrize(
("test_file", "test_mode"), "test_file, test_mode",
[ [
("Tests/images/hopper.jpg", None), ("Tests/images/hopper.jpg", None),
("Tests/images/hopper.jpg", "L"), ("Tests/images/hopper.jpg", "L"),

View File

@ -5,8 +5,6 @@ import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
import pytest
from PIL import Image, PSDraw from PIL import Image, PSDraw
@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None:
assert os.path.getsize(tempfile) > 0 assert os.path.getsize(tempfile) > 0
@pytest.mark.parametrize("buffer", (True, False)) def test_stdout() -> None:
def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout # Temporarily redirect stdout
old_stdout = sys.stdout old_stdout = sys.stdout
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() mystdout = MyStdOut()
sys.stdout = mystdout sys.stdout = mystdout
@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut): assert mystdout.buffer.getvalue() != b""
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""

View File

@ -54,8 +54,8 @@ def test_nonetype() -> None:
assert xres.denominator is not None assert xres.denominator is not None
assert yres._val is not None assert yres._val is not None
assert xres and 1 assert xres
assert xres and yres assert yres
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -118,6 +118,24 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword
arguments can be used instead. arguments can be used instead.
JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They
have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow
12.0.0 (2025-10-15).
Removed features Removed features
---------------- ----------------

View File

@ -14,6 +14,7 @@ from __future__ import annotations
import struct import struct
from io import BytesIO from io import BytesIO
from typing import IO
from PIL import Image, ImageFile from PIL import Image, ImageFile
@ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844
DXT5_FOURCC = 0x35545844 DXT5_FOURCC = 0x35545844
def _decode565(bits): def _decode565(bits: int) -> tuple[int, int, int]:
a = ((bits >> 11) & 0x1F) << 3 a = ((bits >> 11) & 0x1F) << 3
b = ((bits >> 5) & 0x3F) << 2 b = ((bits >> 5) & 0x3F) << 2
c = (bits & 0x1F) << 3 c = (bits & 0x1F) << 3
return a, b, c return a, b, c
def _c2a(a, b): def _c2a(a: int, b: int) -> int:
return (2 * a + b) // 3 return (2 * a + b) // 3
def _c2b(a, b): def _c2b(a: int, b: int) -> int:
return (a + b) // 2 return (a + b) // 2
def _c3(a, b): def _c3(a: int, b: int) -> int:
return (2 * b + a) // 3 return (2 * b + a) // 3
def _dxt1(data, width, height): def _dxt1(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c # TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height) ret = bytearray(4 * width * height)
@ -151,7 +152,7 @@ def _dxt1(data, width, height):
return bytes(ret) return bytes(ret)
def _dxtc_alpha(a0, a1, ac0, ac1, ai): def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int:
if ai <= 12: if ai <= 12:
ac = (ac0 >> ai) & 7 ac = (ac0 >> ai) & 7
elif ai == 15: elif ai == 15:
@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai):
return alpha return alpha
def _dxt5(data, width, height): def _dxt5(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c # TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height) ret = bytearray(4 * width * height)
@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS" format = "DDS"
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a DDS file" msg = "not a DDS file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -242,19 +243,20 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"DXT5": elif fourcc == b"DXT5":
self.decoder = "DXT5" self.decoder = "DXT5"
else: else:
msg = f"Unimplemented pixel format {fourcc}" msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg) raise NotImplementedError(msg)
self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass
class DXT1Decoder(ImageFile.PyDecoder): class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
try: try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
@ -266,7 +268,8 @@ class DXT1Decoder(ImageFile.PyDecoder):
class DXT5Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
try: try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
@ -279,7 +282,7 @@ Image.register_decoder("DXT1", DXT1Decoder)
Image.register_decoder("DXT5", DXT5Decoder) Image.register_decoder("DXT5", DXT5Decoder)
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS " return prefix[:4] == b"DDS "

View File

@ -1220,8 +1220,7 @@ using the general tags available through tiffinfo.
WebP WebP
^^^^ ^^^^
Pillow reads and writes WebP files. The specifics of Pillow's capabilities with Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later.
this format are currently undocumented.
.. _webp-saving: .. _webp-saving:
@ -1249,29 +1248,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exact** **exact**
If true, preserve the transparent RGB values. Otherwise, discard If true, preserve the transparent RGB values. Otherwise, discard
invisible RGB values for better compression. Defaults to false. invisible RGB values for better compression. Defaults to false.
Requires libwebp 0.5.0 or later.
**icc_profile** **icc_profile**
The ICC Profile to include in the saved file. Only supported if The ICC Profile to include in the saved file.
the system WebP library was built with webpmux support.
**exif** **exif**
The exif data to include in the saved file. Only supported if The exif data to include in the saved file.
the system WebP library was built with webpmux support.
**xmp** **xmp**
The XMP data to include in the saved file. Only supported if The XMP data to include in the saved file.
the system WebP library was built with webpmux support.
Saving sequences Saving sequences
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
.. note::
Support for animated WebP files will only be enabled if the system WebP
library is v0.5.0 or later. You can check webp animation support at
runtime by calling ``features.check("webp_anim")``.
When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default
only the first frame of a multiframe image will be saved. If the ``save_all`` only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following argument is present and true, then all frames will be saved, and the following
@ -1528,19 +1517,21 @@ To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler. :: handler. ::
from PIL import Image from typing import IO
from PIL import Image, ImageFile
from PIL import WmfImagePlugin from PIL import WmfImagePlugin
class WmfHandler: class WmfHandler(ImageFile.StubHandler):
def open(self, im): def open(self, im: ImageFile.StubImageFile) -> None:
... ...
def load(self, im): def load(self, im: ImageFile.StubImageFile) -> Image.Image:
... ...
return image return image
def save(self, im, fp, filename): def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
... ...

View File

@ -186,7 +186,7 @@ Rolling an image
:: ::
def roll(im, delta): def roll(im: Image.Image, delta: int) -> Image.Image:
"""Roll an image sideways.""" """Roll an image sideways."""
xsize, ysize = im.size xsize, ysize = im.size
@ -211,7 +211,7 @@ Merging images
:: ::
def merge(im1, im2): def merge(im1: Image.Image, im2: Image.Image) -> Image.Image:
w = im1.size[0] + im2.size[0] w = im1.size[0] + im2.size[0]
h = max(im1.size[1], im2.size[1]) h = max(im1.size[1], im2.size[1])
im = Image.new("RGBA", (w, h)) im = Image.new("RGBA", (w, h))
@ -704,7 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality.
import glob import glob
from PIL import Image from PIL import Image
def compress_image(source_path, dest_path): def compress_image(source_path: str, dest_path: str) -> None:
with Image.open(source_path) as img: with Image.open(source_path) as img:
if img.mode != "RGB": if img.mode != "RGB":
img = img.convert("RGB") img = img.convert("RGB")

View File

@ -53,7 +53,7 @@ true color.
from PIL import Image, ImageFile from PIL import Image, ImageFile
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM" return prefix[:4] == b"SPAM"
@ -62,7 +62,7 @@ true color.
format = "SPAM" format = "SPAM"
format_description = "Spam raster image" format_description = "Spam raster image"
def _open(self): def _open(self) -> None:
header = self.fp.read(128).split() header = self.fp.read(128).split()
@ -82,7 +82,7 @@ true color.
raise SyntaxError(msg) raise SyntaxError(msg)
# data descriptor # data descriptor
self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]
Image.register_open(SpamImageFile.format, SpamImageFile, _accept) Image.register_open(SpamImageFile.format, SpamImageFile, _accept)

View File

@ -55,10 +55,6 @@ Many of Pillow's features require external libraries:
* **libwebp** provides the WebP format. * **libwebp** provides the WebP format.
* Pillow has been tested with version **0.1.3**, which does not read
transparent WebP files. Versions **0.3.0** and above support
transparency.
* **openjpeg** provides JPEG 2000 functionality. * **openjpeg** provides JPEG 2000 functionality.
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
@ -275,18 +271,18 @@ Build Options
* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``,
``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``,
``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, ``-C lcms=disable``, ``-C webp=disable``,
``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``.
Disable building the corresponding feature even if the development Disable building the corresponding feature even if the development
libraries are present on the building machine. libraries are present on the building machine.
* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``,
``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``,
``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, ``-C lcms=enable``, ``-C webp=enable``,
``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``.
Require that the corresponding feature is built. The build will raise Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata) an exception if the libraries are not found. Tcl and Tk must be used
relies on WebP support. Tcl and Tk also must be used together. together.
* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``.
These flags are used to compile a modified version of libraqm and These flags are used to compile a modified version of libraqm and

View File

@ -362,6 +362,7 @@ Classes
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImagePointTransform
.. autoclass:: PIL.Image.ImageTransformHandler .. autoclass:: PIL.Image.ImageTransformHandler
Protocols Protocols

View File

@ -54,12 +54,12 @@ Feature version numbers are available only where stated.
Support for the following features can be checked: Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
* ``transp_webp``: Support for transparency in WebP images.
* ``webp_mux``: (compile time) Support for EXIF data in WebP images.
* ``webp_anim``: (compile time) Support for animated WebP images.
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.
* ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed.
* ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed.
* ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed.
.. autofunction:: PIL.features.check_feature .. autofunction:: PIL.features.check_feature
.. autofunction:: PIL.features.version_feature .. autofunction:: PIL.features.version_feature

View File

@ -185,6 +185,14 @@ Plugin reference
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`~PIL.MpoImagePlugin` Module
----------------------------------
.. automodule:: PIL.MpoImagePlugin
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.MspImagePlugin` Module :mod:`~PIL.MspImagePlugin` Module
--------------------------------- ---------------------------------

View File

@ -50,6 +50,22 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more
keyword arguments can be used instead. keyword arguments can be used instead.
JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They
have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow
12.0.0 (2025-10-15).
API Changes API Changes
=========== ===========
@ -77,3 +93,10 @@ others prepare for 3.13, and to ensure Pillow could be used immediately at the r
of 3.13.0 final (2024-10-01, :pep:`719`). of 3.13.0 final (2024-10-01, :pep:`719`).
Pillow 11.0.0 now officially supports Python 3.13. Pillow 11.0.0 now officially supports Python 3.13.
C-level Flags
^^^^^^^^^^^^^
Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other
``WITH_*`` were removed. These flags were not available through the build system,
but they could be edited in the C source.

View File

@ -109,6 +109,7 @@ lint.select = [
"ISC", # flake8-implicit-str-concat "ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging "LOG", # flake8-logging
"PGH", # pygrep-hooks "PGH", # pygrep-hooks
"PT", # flake8-pytest-style
"PYI", # flake8-pyi "PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa) "RUF100", # unused noqa (yesqa)
"UP", # pyupgrade "UP", # pyupgrade
@ -120,6 +121,12 @@ lint.ignore = [
"E221", # Multiple spaces before operator "E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator "E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ',' "E241", # Multiple spaces after ','
"PT001", # pytest-fixture-incorrect-parentheses-style
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements
"PT016", # pytest-fail-without-message
"PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11 "PYI034", # flake8-pyi: typing.Self added in Python 3.11
] ]
@ -129,6 +136,7 @@ lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [
"I002", "I002",
] ]
lint.flake8-pytest-style.parametrize-names-type = "csv"
lint.isort.known-first-party = [ lint.isort.known-first-party = [
"PIL", "PIL",
] ]

View File

@ -295,7 +295,6 @@ class pil_build_ext(build_ext):
"raqm", "raqm",
"lcms", "lcms",
"webp", "webp",
"webpmux",
"jpeg2000", "jpeg2000",
"imagequant", "imagequant",
"xcb", "xcb",
@ -794,28 +793,18 @@ class pil_build_ext(build_ext):
if feature.want("webp"): if feature.want("webp"):
_dbg("Looking for webp") _dbg("Looking for webp")
if _find_include_file(self, "webp/encode.h") and _find_include_file( if all(
self, "webp/decode.h" _find_include_file(self, "webp/" + include)
for include in ("encode.h", "decode.h", "mux.h", "demux.h")
): ):
# In Google's precompiled zip it is call "libwebp": # In Google's precompiled zip it is called "libwebp"
if _find_library_file(self, "webp"): for prefix in ("", "lib"):
feature.webp = "webp" if all(
elif _find_library_file(self, "libwebp"): _find_library_file(self, prefix + library)
feature.webp = "libwebp" for library in ("webp", "webpmux", "webpdemux")
):
if feature.want("webpmux"): feature.webp = prefix + "webp"
_dbg("Looking for webpmux") break
if _find_include_file(self, "webp/mux.h") and _find_include_file(
self, "webp/demux.h"
):
if _find_library_file(self, "webpmux") and _find_library_file(
self, "webpdemux"
):
feature.webpmux = "webpmux"
if _find_library_file(self, "libwebpmux") and _find_library_file(
self, "libwebpdemux"
):
feature.webpmux = "libwebpmux"
if feature.want("xcb"): if feature.want("xcb"):
_dbg("Looking for xcb") _dbg("Looking for xcb")
@ -904,15 +893,8 @@ class pil_build_ext(build_ext):
self._remove_extension("PIL._imagingcms") self._remove_extension("PIL._imagingcms")
if feature.webp: if feature.webp:
libs = [feature.webp] libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"]
defs = [] self._update_extension("PIL._webp", libs)
if feature.webpmux:
defs.append(("HAVE_WEBPMUX", None))
libs.append(feature.webpmux)
libs.append(feature.webpmux.replace("pmux", "pdemux"))
self._update_extension("PIL._webp", libs, defs)
else: else:
self._remove_extension("PIL._webp") self._remove_extension("PIL._webp")
@ -953,7 +935,6 @@ class pil_build_ext(build_ext):
(feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.raqm, "RAQM (Text shaping)", raqm_extra_info),
(feature.lcms, "LITTLECMS2"), (feature.lcms, "LITTLECMS2"),
(feature.webp, "WEBP"), (feature.webp, "WEBP"),
(feature.webpmux, "WEBPMUX"),
(feature.xcb, "XCB (X protocol)"), (feature.xcb, "XCB (X protocol)"),
] ]

View File

@ -477,7 +477,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(struct.pack("<i", 5)) fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0)) fp.write(struct.pack("<i", 0))
ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)]) ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)])
Image.register_open(BlpImageFile.format, BlpImageFile, _accept) Image.register_open(BlpImageFile.format, BlpImageFile, _accept)

View File

@ -482,7 +482,9 @@ def _save(
if palette: if palette:
fp.write(palette) fp.write(palette)
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
)
# #

View File

@ -65,16 +65,24 @@ def has_ghostscript() -> bool:
return gs_binary is not False return gs_binary is not False
def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: def Ghostscript(
tile: list[ImageFile._Tile],
size: tuple[int, int],
fp: IO[bytes],
scale: int = 1,
transparency: bool = False,
) -> Image.Image:
"""Render an image using Ghostscript""" """Render an image using Ghostscript"""
global gs_binary global gs_binary
if not has_ghostscript(): if not has_ghostscript():
msg = "Unable to locate Ghostscript on paths" msg = "Unable to locate Ghostscript on paths"
raise OSError(msg) raise OSError(msg)
assert isinstance(gs_binary, str)
# Unpack decoder tile # Unpack decoder tile
decoder, tile, offset, data = tile[0] args = tile[0].args
length, bbox = data assert isinstance(args, tuple)
length, bbox = args
# Hack to support hi-res rendering # Hack to support hi-res rendering
scale = int(scale) or 1 scale = int(scale) or 1
@ -227,7 +235,11 @@ class EpsImageFile(ImageFile.ImageFile):
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1] self._size = box[2] - box[0], box[3] - box[1]
self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] self.tile = [
ImageFile._Tile(
"eps", (0, 0) + self.size, offset, (length, box)
)
]
except Exception: except Exception:
pass pass
return True return True
@ -422,7 +434,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()
ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)])
fp.write(b"\n%%%%EndBinary\n") fp.write(b"\n%%%%EndBinary\n")
fp.write(b"grestore end\n") fp.write(b"grestore end\n")

View File

@ -591,7 +591,9 @@ def _write_single_frame(
_write_local_header(fp, im, (0, 0), flags) _write_local_header(fp, im, (0, 0), flags)
im_out.encoderconfig = (8, get_interlace(im)) im_out.encoderconfig = (8, get_interlace(im))
ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]) ImageFile._save(
im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]
)
fp.write(b"\0") # end of image data fp.write(b"\0") # end of image data
@ -1054,7 +1056,9 @@ def _write_frame_data(
_write_local_header(fp, im_frame, offset, 0) _write_local_header(fp, im_frame, offset, 0)
ImageFile._save( ImageFile._save(
im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] im_frame,
fp,
[ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])],
) )
fp.write(b"\0") # end of image data fp.write(b"\0") # end of image data

View File

@ -97,7 +97,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if bits != 32: if bits != 32:
and_mask = Image.new("1", size) and_mask = Image.new("1", size)
ImageFile._save( ImageFile._save(
and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] and_mask,
image_io,
[ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))],
) )
else: else:
frame.save(image_io, "png") frame.save(image_io, "png")
@ -317,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile):
self.load() self.load()
@property @property
def size(self): def size(self) -> tuple[int, int]:
return self._size return self._size
@size.setter @size.setter
def size(self, value): def size(self, value: tuple[int, int]) -> None:
if value not in self.info["sizes"]: if value not in self.info["sizes"]:
msg = "This is not one of the allowed sizes of this image" msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg) raise ValueError(msg)

View File

@ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
palette += im_palette[colors * i : colors * (i + 1)] palette += im_palette[colors * i : colors * (i + 1)]
palette += b"\x00" * (256 - colors) palette += b"\x00" * (256 - colors)
fp.write(palette) # 768 bytes fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
)
# #

View File

@ -218,9 +218,12 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries # Registries
if TYPE_CHECKING: if TYPE_CHECKING:
import mmap
from xml.etree.ElementTree import Element from xml.etree.ElementTree import Element
from . import ImageFile, ImagePalette from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
@ -467,43 +470,53 @@ def _getencoder(
# Simple expression analyzer # Simple expression analyzer
class _E: class ImagePointTransform:
"""
Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than
8 bits, this represents an affine transformation, where the value is multiplied by
``scale`` and ``offset`` is added.
"""
def __init__(self, scale: float, offset: float) -> None: def __init__(self, scale: float, offset: float) -> None:
self.scale = scale self.scale = scale
self.offset = offset self.offset = offset
def __neg__(self) -> _E: def __neg__(self) -> ImagePointTransform:
return _E(-self.scale, -self.offset) return ImagePointTransform(-self.scale, -self.offset)
def __add__(self, other: _E | float) -> _E: def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return _E(self.scale + other.scale, self.offset + other.offset) return ImagePointTransform(
return _E(self.scale, self.offset + other) self.scale + other.scale, self.offset + other.offset
)
return ImagePointTransform(self.scale, self.offset + other)
__radd__ = __add__ __radd__ = __add__
def __sub__(self, other: _E | float) -> _E: def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return self + -other return self + -other
def __rsub__(self, other: _E | float) -> _E: def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return other + -self return other + -self
def __mul__(self, other: _E | float) -> _E: def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return NotImplemented return NotImplemented
return _E(self.scale * other, self.offset * other) return ImagePointTransform(self.scale * other, self.offset * other)
__rmul__ = __mul__ __rmul__ = __mul__
def __truediv__(self, other: _E | float) -> _E: def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return NotImplemented return NotImplemented
return _E(self.scale / other, self.offset / other) return ImagePointTransform(self.scale / other, self.offset / other)
def _getscaleoffset(expr) -> tuple[float, float]: def _getscaleoffset(
a = expr(_E(1, 0)) expr: Callable[[ImagePointTransform], ImagePointTransform | float]
return (a.scale, a.offset) if isinstance(a, _E) else (0, a) ) -> tuple[float, float]:
a = expr(ImagePointTransform(1, 0))
return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -622,7 +635,7 @@ class Image:
logger.debug("Error closing: %s", msg) logger.debug("Error closing: %s", msg)
if getattr(self, "map", None): if getattr(self, "map", None):
self.map = None self.map: mmap.mmap | None = None
# Instead of simply setting to None, we're setting up a # Instead of simply setting to None, we're setting up a
# deferred error that will better explain that the core image # deferred error that will better explain that the core image
@ -686,7 +699,7 @@ class Image:
id(self), id(self),
) )
def _repr_pretty_(self, p, cycle) -> None: def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
"""IPython plain text display support""" """IPython plain text display support"""
# Same as __repr__ but without unpredictable id(self), # Same as __repr__ but without unpredictable id(self),
@ -733,24 +746,12 @@ class Image:
def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]:
# numpy array interface support # numpy array interface support
new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3}
try: if self.mode == "1":
if self.mode == "1": # Binary images need to be extended from bits to bytes
# Binary images need to be extended from bits to bytes # See: https://github.com/python-pillow/Pillow/issues/350
# See: https://github.com/python-pillow/Pillow/issues/350 new["data"] = self.tobytes("raw", "L")
new["data"] = self.tobytes("raw", "L") else:
else: new["data"] = self.tobytes()
new["data"] = self.tobytes()
except Exception as e:
if not isinstance(e, (MemoryError, RecursionError)):
try:
import numpy
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if parse_version(numpy.__version__) < parse_version("1.23"):
warnings.warn(str(e))
raise
new["shape"], new["typestr"] = _conv_type_shape(self) new["shape"], new["typestr"] = _conv_type_shape(self)
return new return new
@ -1346,9 +1347,6 @@ class Image:
self.load() self.load()
return self._new(self.im.expand(xmargin, ymargin)) return self._new(self.im.expand(xmargin, ymargin))
if TYPE_CHECKING:
from . import ImageFilter
def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
""" """
Filters this image using the given filter. For a list of Filters this image using the given filter. For a list of
@ -1561,6 +1559,7 @@ class Image:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(513): if ifd1 and ifd1.get(513):
assert exif._info is not None
ifds.append((ifd1, exif._info.next)) ifds.append((ifd1, exif._info.next))
offset = None offset = None
@ -1570,12 +1569,13 @@ class Image:
offset = current_offset offset = current_offset
fp = self.fp fp = self.fp
thumbnail_offset = ifd.get(513) if ifd is not None:
if thumbnail_offset is not None: thumbnail_offset = ifd.get(513)
thumbnail_offset += getattr(self, "_exif_offset", 0) if thumbnail_offset is not None:
self.fp.seek(thumbnail_offset) thumbnail_offset += getattr(self, "_exif_offset", 0)
data = self.fp.read(ifd.get(514)) self.fp.seek(thumbnail_offset)
fp = io.BytesIO(data) data = self.fp.read(ifd.get(514))
fp = io.BytesIO(data)
with open(fp) as im: with open(fp) as im:
from . import TiffImagePlugin from . import TiffImagePlugin
@ -1902,7 +1902,13 @@ class Image:
def point( def point(
self, self,
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, lut: (
Sequence[float]
| NumpyArray
| Callable[[int], float]
| Callable[[ImagePointTransform], ImagePointTransform | float]
| ImagePointHandler
),
mode: str | None = None, mode: str | None = None,
) -> Image: ) -> Image:
""" """
@ -1919,7 +1925,7 @@ class Image:
object:: object::
class Example(Image.ImagePointHandler): class Example(Image.ImagePointHandler):
def point(self, data): def point(self, im: Image) -> Image:
# Return result # Return result
:param mode: Output mode (default is same as input). This can only be used if :param mode: Output mode (default is same as input). This can only be used if
the source image has mode "L" or "P", and the output has mode "1" or the the source image has mode "L" or "P", and the output has mode "1" or the
@ -1938,10 +1944,10 @@ class Image:
# check if the function can be used with point_transform # check if the function can be used with point_transform
# UNDONE wiredfool -- I think this prevents us from ever doing # UNDONE wiredfool -- I think this prevents us from ever doing
# a gamma function point transform on > 8bit images. # a gamma function point transform on > 8bit images.
scale, offset = _getscaleoffset(lut) scale, offset = _getscaleoffset(lut) # type: ignore[arg-type]
return self._new(self.im.point_transform(scale, offset)) return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table # for other modes, convert the function to a table
flatLut = [lut(i) for i in range(256)] * self.im.bands flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type]
else: else:
flatLut = lut flatLut = lut
@ -2062,7 +2068,11 @@ class Image:
msg = "illegal image mode" msg = "illegal image mode"
raise ValueError(msg) raise ValueError(msg)
if isinstance(data, ImagePalette.ImagePalette): if isinstance(data, ImagePalette.ImagePalette):
palette = ImagePalette.raw(data.rawmode, data.palette) if data.rawmode is not None:
palette = ImagePalette.raw(data.rawmode, data.palette)
else:
palette = ImagePalette.ImagePalette(palette=data.palette)
palette.dirty = 1
else: else:
if not isinstance(data, bytes): if not isinstance(data, bytes):
data = bytes(data) data = bytes(data)
@ -2873,11 +2883,11 @@ class Image:
self, self,
box: tuple[int, int, int, int], box: tuple[int, int, int, int],
image: Image, image: Image,
method, method: Transform,
data, data: Sequence[float],
resample: int = Resampling.NEAREST, resample: int = Resampling.NEAREST,
fill: bool = True, fill: bool = True,
): ) -> None:
w = box[2] - box[0] w = box[2] - box[0]
h = box[3] - box[1] h = box[3] - box[1]
@ -3878,14 +3888,14 @@ class Exif(_ExifBase):
bigtiff = False bigtiff = False
_loaded = False _loaded = False
def __init__(self): def __init__(self) -> None:
self._data = {} self._data: dict[int, Any] = {}
self._hidden_data = {} self._hidden_data: dict[int, Any] = {}
self._ifds = {} self._ifds: dict[int, dict[int, Any]] = {}
self._info = None self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None
self._loaded_exif = None self._loaded_exif: bytes | None = None
def _fixup(self, value): def _fixup(self, value: Any) -> Any:
try: try:
if len(value) == 1 and isinstance(value, tuple): if len(value) == 1 and isinstance(value, tuple):
return value[0] return value[0]
@ -3893,24 +3903,26 @@ class Exif(_ExifBase):
pass pass
return value return value
def _fixup_dict(self, src_dict): def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]:
# Helper function # Helper function
# returns a dict with any single item tuples/lists as individual values # returns a dict with any single item tuples/lists as individual values
return {k: self._fixup(v) for k, v in src_dict.items()} return {k: self._fixup(v) for k, v in src_dict.items()}
def _get_ifd_dict(self, offset: int, group: int | None = None): def _get_ifd_dict(
self, offset: int, group: int | None = None
) -> dict[int, Any] | None:
try: try:
# an offset pointer to the location of the nested embedded IFD. # an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted. # It should be a long, but may be corrupted.
self.fp.seek(offset) self.fp.seek(offset)
except (KeyError, TypeError): except (KeyError, TypeError):
pass return None
else: else:
from . import TiffImagePlugin from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group)
info.load(self.fp) info.load(self.fp)
return self._fixup_dict(info) return self._fixup_dict(dict(info))
def _get_head(self) -> bytes: def _get_head(self) -> bytes:
version = b"\x2B" if self.bigtiff else b"\x2A" version = b"\x2B" if self.bigtiff else b"\x2A"
@ -3975,7 +3987,7 @@ class Exif(_ExifBase):
self.fp.seek(offset) self.fp.seek(offset)
self._info.load(self.fp) self._info.load(self.fp)
def _get_merged_dict(self): def _get_merged_dict(self) -> dict[int, Any]:
merged_dict = dict(self) merged_dict = dict(self)
# get EXIF extension # get EXIF extension
@ -4013,15 +4025,19 @@ class Exif(_ExifBase):
ifd[tag] = value ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset) return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag): def get_ifd(self, tag: int) -> dict[int, Any]:
if tag not in self._ifds: if tag not in self._ifds:
if tag == ExifTags.IFD.IFD1: if tag == ExifTags.IFD.IFD1:
if self._info is not None and self._info.next != 0: if self._info is not None and self._info.next != 0:
self._ifds[tag] = self._get_ifd_dict(self._info.next) ifd = self._get_ifd_dict(self._info.next)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
offset = self._hidden_data.get(tag, self.get(tag)) offset = self._hidden_data.get(tag, self.get(tag))
if offset is not None: if offset is not None:
self._ifds[tag] = self._get_ifd_dict(offset, tag) ifd = self._get_ifd_dict(offset, tag)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds: if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif) self.get_ifd(ExifTags.IFD.Exif)
@ -4078,7 +4094,9 @@ class Exif(_ExifBase):
(offset,) = struct.unpack(">L", data) (offset,) = struct.unpack(">L", data)
self.fp.seek(offset) self.fp.seek(offset)
camerainfo = {"ModelID": self.fp.read(4)} camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
self.fp.read(4) self.fp.read(4)
# Seconds since 2000 # Seconds since 2000
@ -4094,16 +4112,18 @@ class Exif(_ExifBase):
][1] ][1]
camerainfo["Parallax"] = handler( camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False ImageFileDirectory_v2(), parallax, False
) )[0]
self.fp.read(4) self.fp.read(4)
camerainfo["Category"] = self.fp.read(2) camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))} makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote self._ifds[tag] = makernote
else: else:
# Interop # Interop
self._ifds[tag] = self._get_ifd_dict(tag_data, tag) ifd = self._get_ifd_dict(tag_data, tag)
if ifd is not None:
self._ifds[tag] = ifd
ifd = self._ifds.get(tag, {}) ifd = self._ifds.get(tag, {})
if tag == ExifTags.IFD.Exif and self._hidden_data: if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = { ifd = {
@ -4133,7 +4153,7 @@ class Exif(_ExifBase):
keys.update(self._info) keys.update(self._info)
return len(keys) return len(keys)
def __getitem__(self, tag: int): def __getitem__(self, tag: int) -> Any:
if self._info is not None and tag not in self._data and tag in self._info: 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]) self._data[tag] = self._fixup(self._info[tag])
del self._info[tag] del self._info[tag]
@ -4142,7 +4162,7 @@ class Exif(_ExifBase):
def __contains__(self, tag: object) -> bool: def __contains__(self, tag: object) -> bool:
return tag in self._data or (self._info is not None and tag in self._info) return tag in self._data or (self._info is not None and tag in self._info)
def __setitem__(self, tag: int, value) -> None: def __setitem__(self, tag: int, value: Any) -> None:
if self._info is not None and tag in self._info: if self._info is not None and tag in self._info:
del self._info[tag] del self._info[tag]
self._data[tag] = value self._data[tag] = value

View File

@ -505,7 +505,7 @@ class ImageDraw:
if full_x: if full_x:
self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
else: elif x1 - r - 1 > x0 + r + 1:
self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
if not full_x and not full_y: if not full_x and not full_y:
left = [x0, y0, x0 + r, y1] left = [x0, y0, x0 + r, y1]

View File

@ -80,7 +80,12 @@ class Draw:
return self.image return self.image
def render( def render(
self, op: str, xy: Coords, pen: Pen | Brush, brush: Brush | Pen | None = None self,
op: str,
xy: Coords,
pen: Pen | Brush | None,
brush: Brush | Pen | None = None,
**kwargs: Any,
) -> None: ) -> None:
# handle color arguments # handle color arguments
outline = fill = None outline = fill = None
@ -101,60 +106,85 @@ class Draw:
path.transform(self.transform) path.transform(self.transform)
xy = path xy = path
# render the item # render the item
if op == "line": if op in ("arc", "line"):
self.draw.line(xy, fill=outline, width=width) kwargs.setdefault("fill", outline)
else: else:
getattr(self.draw, op)(xy, fill=fill, outline=outline) kwargs.setdefault("fill", fill)
kwargs.setdefault("outline", outline)
if op == "line":
kwargs.setdefault("width", width)
getattr(self.draw, op)(xy, **kwargs)
def settransform(self, offset: tuple[float, float]) -> None: def settransform(self, offset: tuple[float, float]) -> None:
"""Sets a transformation offset.""" """Sets a transformation offset."""
(xoffset, yoffset) = offset (xoffset, yoffset) = offset
self.transform = (1, 0, xoffset, 0, 1, yoffset) self.transform = (1, 0, xoffset, 0, 1, yoffset)
def arc(self, xy: Coords, start, end, *options: Any) -> None: def arc(
self,
xy: Coords,
pen: Pen | Brush | None,
start: float,
end: float,
*options: Any,
) -> None:
""" """
Draws an arc (a portion of a circle outline) between the start and end Draws an arc (a portion of a circle outline) between the start and end
angles, inside the given bounding box. angles, inside the given bounding box.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc`
""" """
self.render("arc", xy, start, end, *options) self.render("arc", xy, pen, *options, start=start, end=end)
def chord(self, xy: Coords, start, end, *options: Any) -> None: def chord(
self,
xy: Coords,
pen: Pen | Brush | None,
start: float,
end: float,
*options: Any,
) -> None:
""" """
Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points
with a straight line. with a straight line.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord`
""" """
self.render("chord", xy, start, end, *options) self.render("chord", xy, pen, *options, start=start, end=end)
def ellipse(self, xy: Coords, *options: Any) -> None: def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws an ellipse inside the given bounding box. Draws an ellipse inside the given bounding box.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse`
""" """
self.render("ellipse", xy, *options) self.render("ellipse", xy, pen, *options)
def line(self, xy: Coords, *options: Any) -> None: def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws a line between the coordinates in the ``xy`` list. Draws a line between the coordinates in the ``xy`` list.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line`
""" """
self.render("line", xy, *options) self.render("line", xy, pen, *options)
def pieslice(self, xy: Coords, start, end, *options: Any) -> None: def pieslice(
self,
xy: Coords,
pen: Pen | Brush | None,
start: float,
end: float,
*options: Any,
) -> None:
""" """
Same as arc, but also draws straight lines between the end points and the Same as arc, but also draws straight lines between the end points and the
center of the bounding box. center of the bounding box.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice`
""" """
self.render("pieslice", xy, start, end, *options) self.render("pieslice", xy, pen, *options, start=start, end=end)
def polygon(self, xy: Coords, *options: Any) -> None: def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws a polygon. Draws a polygon.
@ -165,15 +195,15 @@ class Draw:
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon`
""" """
self.render("polygon", xy, *options) self.render("polygon", xy, pen, *options)
def rectangle(self, xy: Coords, *options) -> None: def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
""" """
Draws a rectangle. Draws a rectangle.
.. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle`
""" """
self.render("rectangle", xy, *options) self.render("rectangle", xy, pen, *options)
def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None:
""" """

View File

@ -31,6 +31,7 @@ from __future__ import annotations
import abc import abc
import io import io
import itertools import itertools
import os
import struct import struct
import sys import sys
from typing import IO, Any, NamedTuple from typing import IO, Any, NamedTuple
@ -86,14 +87,14 @@ def raise_oserror(error: int) -> OSError:
raise _get_oserror(error, encoder=False) raise _get_oserror(error, encoder=False)
def _tilesort(t) -> int: def _tilesort(t: _Tile) -> int:
# sort on offset # sort on offset
return t[2] return t[2]
class _Tile(NamedTuple): class _Tile(NamedTuple):
codec_name: str codec_name: str
extents: tuple[int, int, int, int] extents: tuple[int, int, int, int] | None
offset: int offset: int
args: tuple[Any, ...] | str | None args: tuple[Any, ...] | str | None
@ -161,7 +162,7 @@ class ImageFile(Image.Image):
return Image.MIME.get(self.format.upper()) return Image.MIME.get(self.format.upper())
return None return None
def __setstate__(self, state) -> None: def __setstate__(self, state: list[Any]) -> None:
self.tile = [] self.tile = []
super().__setstate__(state) super().__setstate__(state)
@ -174,7 +175,7 @@ class ImageFile(Image.Image):
self.fp.close() self.fp.close()
self.fp = None self.fp = None
def load(self): def load(self) -> Image.core.PixelAccess | None:
"""Load image data based on tile list""" """Load image data based on tile list"""
if self.tile is None: if self.tile is None:
@ -185,7 +186,7 @@ class ImageFile(Image.Image):
if not self.tile: if not self.tile:
return pixel return pixel
self.map = None self.map: mmap.mmap | None = None
use_mmap = self.filename and len(self.tile) == 1 use_mmap = self.filename and len(self.tile) == 1
# As of pypy 2.1.0, memory mapping was failing here. # As of pypy 2.1.0, memory mapping was failing here.
use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") use_mmap = use_mmap and not hasattr(sys, "pypy_version_info")
@ -193,17 +194,17 @@ class ImageFile(Image.Image):
readonly = 0 readonly = 0
# look for read/seek overrides # look for read/seek overrides
try: if hasattr(self, "load_read"):
read = self.load_read read = self.load_read
# don't use mmap if there are custom read/seek functions # don't use mmap if there are custom read/seek functions
use_mmap = False use_mmap = False
except AttributeError: else:
read = self.fp.read read = self.fp.read
try: if hasattr(self, "load_seek"):
seek = self.load_seek seek = self.load_seek
use_mmap = False use_mmap = False
except AttributeError: else:
seek = self.fp.seek seek = self.fp.seek
if use_mmap: if use_mmap:
@ -243,11 +244,8 @@ class ImageFile(Image.Image):
# sort tiles in file order # sort tiles in file order
self.tile.sort(key=_tilesort) self.tile.sort(key=_tilesort)
try: # FIXME: This is a hack to handle TIFF's JpegTables tag.
# FIXME: This is a hack to handle TIFF's JpegTables tag. prefix = getattr(self, "tile_prefix", b"")
prefix = self.tile_prefix
except AttributeError:
prefix = b""
# Remove consecutive duplicates that only differ by their offset # Remove consecutive duplicates that only differ by their offset
self.tile = [ self.tile = [
@ -525,7 +523,7 @@ class Parser:
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im, fp, tile, bufsize: int = 0) -> None: def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None:
"""Helper to save image based on tile list """Helper to save image based on tile list
:param im: Image object. :param im: Image object.
@ -554,7 +552,12 @@ def _save(im, fp, tile, bufsize: int = 0) -> None:
def _encode_tile( def _encode_tile(
im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None im: Image.Image,
fp: IO[bytes],
tile: list[_Tile],
bufsize: int,
fh: int | None,
exc: BaseException | None = None,
) -> None: ) -> None:
for encoder_name, extents, offset, args in tile: for encoder_name, extents, offset, args in tile:
if offset > 0: if offset > 0:
@ -575,6 +578,7 @@ def _encode_tile(
break break
else: else:
# slight speedup: compress to real file object # slight speedup: compress to real file object
assert fh is not None
errcode = encoder.encode_to_file(fh, bufsize) errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0: if errcode < 0:
raise _get_oserror(errcode, encoder=True) from exc raise _get_oserror(errcode, encoder=True) from exc
@ -664,7 +668,11 @@ class PyCodec:
""" """
self.fd = fd self.fd = fd
def setimage(self, im, extents=None): def setimage(
self,
im: Image.core.ImagingCore,
extents: tuple[int, int, int, int] | None = None,
) -> None:
""" """
Called from ImageFile to set the core output image for the codec Called from ImageFile to set the core output image for the codec
@ -795,7 +803,7 @@ class PyEncoder(PyCodec):
self.fd.write(data) self.fd.write(data)
return bytes_consumed, errcode return bytes_consumed, errcode
def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: def encode_to_file(self, fh: int, bufsize: int) -> int:
""" """
:param fh: File handle. :param fh: File handle.
:param bufsize: Buffer size. :param bufsize: Buffer size.
@ -808,5 +816,5 @@ class PyEncoder(PyCodec):
while errcode == 0: while errcode == 0:
status, errcode, buf = self.encode(bufsize) status, errcode, buf = self.encode(bufsize)
if status > 0: if status > 0:
fh.write(buf[status:]) os.write(fh, buf[status:])
return errcode return errcode

View File

@ -269,12 +269,12 @@ class FreeTypeFont:
else: else:
load_from_bytes(font) load_from_bytes(font)
def __getstate__(self): def __getstate__(self) -> list[Any]:
return [self.path, self.size, self.index, self.encoding, self.layout_engine] return [self.path, self.size, self.index, self.encoding, self.layout_engine]
def __setstate__(self, state): def __setstate__(self, state: list[Any]) -> None:
path, size, index, encoding, layout_engine = state path, size, index, encoding, layout_engine = state
self.__init__(path, size, index, encoding, layout_engine) FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine)
def getname(self) -> tuple[str | None, str | None]: def getname(self) -> tuple[str | None, str | None]:
""" """

View File

@ -208,7 +208,7 @@ class ImagePalette:
# Internal # Internal
def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
palette = ImagePalette() palette = ImagePalette()
palette.rawmode = rawmode palette.rawmode = rawmode
palette.palette = data palette.palette = data

View File

@ -213,8 +213,8 @@ def getiptcinfo(
# get raw data from the IPTC/NAA tag (PhotoShop tags the data # get raw data from the IPTC/NAA tag (PhotoShop tags the data
# as 4-byte integers, so we cannot use the get method...) # as 4-byte integers, so we cannot use the get method...)
try: try:
data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK]
except (AttributeError, KeyError): except KeyError:
pass pass
if data is None: if data is None:

View File

@ -324,7 +324,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
return self._reduce or super().reduce return self._reduce or super().reduce
@reduce.setter @reduce.setter
def reduce(self, value): def reduce(self, value: int) -> None:
self._reduce = value self._reduce = value
def load(self) -> Image.core.PixelAccess | None: def load(self) -> Image.core.PixelAccess | None:
@ -419,7 +419,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
plt, plt,
) )
ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)])
# ------------------------------------------------------------ # ------------------------------------------------------------

View File

@ -42,15 +42,19 @@ import subprocess
import sys import sys
import tempfile import tempfile
import warnings import warnings
from typing import IO, Any from typing import IO, TYPE_CHECKING, Any
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16 from ._binary import o16be as o16
from ._deprecate import deprecate
from .JpegPresets import presets from .JpegPresets import presets
if TYPE_CHECKING:
from .MpoImagePlugin import MpoImageFile
# #
# Parser # Parser
@ -329,7 +333,7 @@ class JpegImageFile(ImageFile.ImageFile):
format = "JPEG" format = "JPEG"
format_description = "JPEG (ISO 10918)" format_description = "JPEG (ISO 10918)"
def _open(self): def _open(self) -> None:
s = self.fp.read(3) s = self.fp.read(3)
if not _accept(s): if not _accept(s):
@ -342,13 +346,13 @@ class JpegImageFile(ImageFile.ImageFile):
self._exif_offset = 0 self._exif_offset = 0
# JPEG specifics (internal) # JPEG specifics (internal)
self.layer = [] self.layer: list[tuple[int, int, int, int]] = []
self.huffman_dc = {} self._huffman_dc: dict[Any, Any] = {}
self.huffman_ac = {} self._huffman_ac: dict[Any, Any] = {}
self.quantization = {} self.quantization: dict[int, list[int]] = {}
self.app = {} # compatibility self.app: dict[str, bytes] = {} # compatibility
self.applist = [] self.applist: list[tuple[str, bytes]] = []
self.icclist = [] self.icclist: list[bytes] = []
while True: while True:
i = s[0] i = s[0]
@ -383,6 +387,12 @@ class JpegImageFile(ImageFile.ImageFile):
self._read_dpi_from_exif() self._read_dpi_from_exif()
def __getattr__(self, name: str) -> Any:
if name in ("huffman_ac", "huffman_dc"):
deprecate(name, 12)
return getattr(self, "_" + name)
raise AttributeError(name)
def load_read(self, read_bytes: int) -> bytes: def load_read(self, read_bytes: int) -> bytes:
""" """
internal: read more image data internal: read more image data
@ -816,7 +826,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Ensure that our buffer is big enough. Same with the icc_profile block. # Ensure that our buffer is big enough. Same with the icc_profile block.
bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) bufsize = max(bufsize, len(exif) + 5, len(extra) + 1)
ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) ImageFile._save(
im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
)
def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
@ -831,7 +843,9 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
## ##
# Factory for making JPEG and MPO instances # Factory for making JPEG and MPO instances
def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None): def jpeg_factory(
fp: IO[bytes] | None = None, filename: str | bytes | None = None
) -> JpegImageFile | MpoImageFile:
im = JpegImageFile(fp, filename) im = JpegImageFile(fp, filename)
try: try:
mpheader = im._getmp() mpheader = im._getmp()

View File

@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(o16(h)) fp.write(o16(h))
# image body # image body
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))])
# #

View File

@ -17,7 +17,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import TYPE_CHECKING from typing import IO, TYPE_CHECKING
from . import EpsImagePlugin from . import EpsImagePlugin
@ -28,15 +28,12 @@ from . import EpsImagePlugin
class PSDraw: class PSDraw:
""" """
Sets up printing to the given file. If ``fp`` is omitted, Sets up printing to the given file. If ``fp`` is omitted,
``sys.stdout.buffer`` or ``sys.stdout`` is assumed. ``sys.stdout.buffer`` is assumed.
""" """
def __init__(self, fp=None): def __init__(self, fp: IO[bytes] | None = None) -> None:
if not fp: if not fp:
try: fp = sys.stdout.buffer
fp = sys.stdout.buffer
except AttributeError:
fp = sys.stdout
self.fp = fp self.fp = fp
def begin_document(self, id: str | None = None) -> None: def begin_document(self, id: str | None = None) -> None:

View File

@ -213,7 +213,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
) )
# now convert data to raw form # now convert data to raw form
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]
)
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()

View File

@ -198,7 +198,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
assert fp.tell() == 128 assert fp.tell() == 128
ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) ImageFile._save(
im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]
)
if im.mode == "P": if im.mode == "P":
# colour palette # colour palette

View File

@ -25,7 +25,7 @@ import io
import math import math
import os import os
import time import time
from typing import IO from typing import IO, Any
from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
@ -48,7 +48,12 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# (Internal) Image save plugin for the PDF format. # (Internal) Image save plugin for the PDF format.
def _write_image(im, filename, existing_pdf, image_refs): def _write_image(
im: Image.Image,
filename: str | bytes,
existing_pdf: PdfParser.PdfParser,
image_refs: list[PdfParser.IndirectReference],
) -> tuple[PdfParser.IndirectReference, str]:
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
# (packbits) or LZWDecode (tiff/lzw compression). Note that # (packbits) or LZWDecode (tiff/lzw compression). Note that
# PDF 1.2 also supports Flatedecode (zip compression). # PDF 1.2 also supports Flatedecode (zip compression).
@ -61,10 +66,10 @@ def _write_image(im, filename, existing_pdf, image_refs):
width, height = im.size width, height = im.size
dict_obj = {"BitsPerComponent": 8} dict_obj: dict[str, Any] = {"BitsPerComponent": 8}
if im.mode == "1": if im.mode == "1":
if features.check("libtiff"): if features.check("libtiff"):
filter = "CCITTFaxDecode" decode_filter = "CCITTFaxDecode"
dict_obj["BitsPerComponent"] = 1 dict_obj["BitsPerComponent"] = 1
params = PdfParser.PdfArray( params = PdfParser.PdfArray(
[ [
@ -79,22 +84,23 @@ def _write_image(im, filename, existing_pdf, image_refs):
] ]
) )
else: else:
filter = "DCTDecode" decode_filter = "DCTDecode"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
elif im.mode == "L": elif im.mode == "L":
filter = "DCTDecode" decode_filter = "DCTDecode"
# params = f"<< /Predictor 15 /Columns {width-2} >>" # params = f"<< /Predictor 15 /Columns {width-2} >>"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
elif im.mode == "LA": elif im.mode == "LA":
filter = "JPXDecode" decode_filter = "JPXDecode"
# params = f"<< /Predictor 15 /Columns {width-2} >>" # params = f"<< /Predictor 15 /Columns {width-2} >>"
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
dict_obj["SMaskInData"] = 1 dict_obj["SMaskInData"] = 1
elif im.mode == "P": elif im.mode == "P":
filter = "ASCIIHexDecode" decode_filter = "ASCIIHexDecode"
palette = im.getpalette() palette = im.getpalette()
assert palette is not None
dict_obj["ColorSpace"] = [ dict_obj["ColorSpace"] = [
PdfParser.PdfName("Indexed"), PdfParser.PdfName("Indexed"),
PdfParser.PdfName("DeviceRGB"), PdfParser.PdfName("DeviceRGB"),
@ -110,15 +116,15 @@ def _write_image(im, filename, existing_pdf, image_refs):
image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0]
dict_obj["SMask"] = image_ref dict_obj["SMask"] = image_ref
elif im.mode == "RGB": elif im.mode == "RGB":
filter = "DCTDecode" decode_filter = "DCTDecode"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
procset = "ImageC" # color images procset = "ImageC" # color images
elif im.mode == "RGBA": elif im.mode == "RGBA":
filter = "JPXDecode" decode_filter = "JPXDecode"
procset = "ImageC" # color images procset = "ImageC" # color images
dict_obj["SMaskInData"] = 1 dict_obj["SMaskInData"] = 1
elif im.mode == "CMYK": elif im.mode == "CMYK":
filter = "DCTDecode" decode_filter = "DCTDecode"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
procset = "ImageC" # color images procset = "ImageC" # color images
decode = [1, 0, 1, 0, 1, 0, 1, 0] decode = [1, 0, 1, 0, 1, 0, 1, 0]
@ -131,9 +137,9 @@ def _write_image(im, filename, existing_pdf, image_refs):
op = io.BytesIO() op = io.BytesIO()
if filter == "ASCIIHexDecode": if decode_filter == "ASCIIHexDecode":
ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)])
elif filter == "CCITTFaxDecode": elif decode_filter == "CCITTFaxDecode":
im.save( im.save(
op, op,
"TIFF", "TIFF",
@ -141,21 +147,22 @@ def _write_image(im, filename, existing_pdf, image_refs):
# use a single strip # use a single strip
strip_size=math.ceil(width / 8) * height, strip_size=math.ceil(width / 8) * height,
) )
elif filter == "DCTDecode": elif decode_filter == "DCTDecode":
Image.SAVE["JPEG"](im, op, filename) Image.SAVE["JPEG"](im, op, filename)
elif filter == "JPXDecode": elif decode_filter == "JPXDecode":
del dict_obj["BitsPerComponent"] del dict_obj["BitsPerComponent"]
Image.SAVE["JPEG2000"](im, op, filename) Image.SAVE["JPEG2000"](im, op, filename)
else: else:
msg = f"unsupported PDF filter ({filter})" msg = f"unsupported PDF filter ({decode_filter})"
raise ValueError(msg) raise ValueError(msg)
stream = op.getvalue() stream = op.getvalue()
if filter == "CCITTFaxDecode": filter: PdfParser.PdfArray | PdfParser.PdfName
if decode_filter == "CCITTFaxDecode":
stream = stream[8:] stream = stream[8:]
filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)])
else: else:
filter = PdfParser.PdfName(filter) filter = PdfParser.PdfName(decode_filter)
image_ref = image_refs.pop(0) image_ref = image_refs.pop(0)
existing_pdf.write_obj( existing_pdf.write_obj(

View File

@ -40,7 +40,7 @@ import warnings
import zlib import zlib
from collections.abc import Callable from collections.abc import Callable
from enum import IntEnum from enum import IntEnum
from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -1223,7 +1223,11 @@ def _write_multiple_frames(
if default_image: if default_image:
if im.mode != mode: if im.mode != mode:
im = im.convert(mode) im = im.convert(mode)
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) ImageFile._save(
im,
cast(IO[bytes], _idat(fp, chunk)),
[ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)],
)
seq_num = 0 seq_num = 0
for frame, frame_data in enumerate(im_frames): for frame, frame_data in enumerate(im_frames):
@ -1258,15 +1262,15 @@ def _write_multiple_frames(
# first frame must be in IDAT chunks for backwards compatibility # first frame must be in IDAT chunks for backwards compatibility
ImageFile._save( ImageFile._save(
im_frame, im_frame,
_idat(fp, chunk), cast(IO[bytes], _idat(fp, chunk)),
[("zip", (0, 0) + im_frame.size, 0, rawmode)], [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
) )
else: else:
fdat_chunks = _fdat(fp, chunk, seq_num) fdat_chunks = _fdat(fp, chunk, seq_num)
ImageFile._save( ImageFile._save(
im_frame, im_frame,
fdat_chunks, cast(IO[bytes], fdat_chunks),
[("zip", (0, 0) + im_frame.size, 0, rawmode)], [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
) )
seq_num = fdat_chunks.seq_num seq_num = fdat_chunks.seq_num
return None return None
@ -1465,7 +1469,9 @@ def _save(
) )
if single_im: if single_im:
ImageFile._save( ImageFile._save(
single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)] single_im,
cast(IO[bytes], _idat(fp, chunk)),
[ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
) )
if info: if info:

View File

@ -353,7 +353,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
elif head == b"Pf": elif head == b"Pf":
fp.write(b"-1.0\n") fp.write(b"-1.0\n")
row_order = -1 if im.mode == "F" else 1 row_order = -1 if im.mode == "F" else 1
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]
)
# #

View File

@ -278,7 +278,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.writelines(hdr) fp.writelines(hdr)
rawmode = "F;32NF" # 32-bit native floating point rawmode = "F;32NF" # 32-bit native floating point
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

View File

@ -238,11 +238,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if rle: if rle:
ImageFile._save( ImageFile._save(
im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] im,
fp,
[ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))],
) )
else: else:
ImageFile._save( ImageFile._save(
im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] im,
fp,
[ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))],
) )
# write targa version 2 footer # write targa version 2 footer

View File

@ -47,16 +47,18 @@ import math
import os import os
import struct import struct
import warnings import warnings
from collections.abc import MutableMapping from collections.abc import Iterator, MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._deprecate import deprecate from ._deprecate import deprecate
from ._typing import StrOrBytesPath
from ._util import is_path
from .TiffTags import TYPES from .TiffTags import TYPES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -257,6 +259,7 @@ OPEN_INFO = {
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
(II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
(MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"),
(II, 6, (1,), 1, (8,), ()): ("L", "L"), (II, 6, (1,), 1, (8,), ()): ("L", "L"),
(MM, 6, (1,), 1, (8,), ()): ("L", "L"), (MM, 6, (1,), 1, (8,), ()): ("L", "L"),
# JPEG compressed images handled by LibTiff and auto-converted to RGBX # JPEG compressed images handled by LibTiff and auto-converted to RGBX
@ -286,8 +289,10 @@ def _accept(prefix: bytes) -> bool:
return prefix[:4] in PREFIXES return prefix[:4] in PREFIXES
def _limit_rational(val, max_val): def _limit_rational(
inv = abs(val) > 1 val: float | Fraction | IFDRational, max_val: int
) -> tuple[float, float]:
inv = abs(float(val)) > 1
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
return n_d[::-1] if inv else n_d return n_d[::-1] if inv else n_d
@ -313,7 +318,7 @@ _load_dispatch = {}
_write_dispatch = {} _write_dispatch = {}
def _delegate(op): def _delegate(op: str):
def delegate(self, *args): def delegate(self, *args):
return getattr(self._val, op)(*args) return getattr(self._val, op)(*args)
@ -334,7 +339,9 @@ class IFDRational(Rational):
__slots__ = ("_numerator", "_denominator", "_val") __slots__ = ("_numerator", "_denominator", "_val")
def __init__(self, value, denominator: int = 1) -> None: def __init__(
self, value: float | Fraction | IFDRational, denominator: int = 1
) -> None:
""" """
:param value: either an integer numerator, a :param value: either an integer numerator, a
float/rational/other number, or an IFDRational float/rational/other number, or an IFDRational
@ -358,18 +365,20 @@ class IFDRational(Rational):
self._val = float("nan") self._val = float("nan")
elif denominator == 1: elif denominator == 1:
self._val = Fraction(value) self._val = Fraction(value)
elif int(value) == value:
self._val = Fraction(int(value), denominator)
else: else:
self._val = Fraction(value, denominator) self._val = Fraction(value / denominator)
@property @property
def numerator(self): def numerator(self):
return self._numerator return self._numerator
@property @property
def denominator(self): def denominator(self) -> int:
return self._denominator return self._denominator
def limit_rational(self, max_denominator): def limit_rational(self, max_denominator: int) -> tuple[float, int]:
""" """
:param max_denominator: Integer, the maximum denominator value :param max_denominator: Integer, the maximum denominator value
@ -379,6 +388,7 @@ class IFDRational(Rational):
if self.denominator == 0: if self.denominator == 0:
return self.numerator, self.denominator return self.numerator, self.denominator
assert isinstance(self._val, Fraction)
f = self._val.limit_denominator(max_denominator) f = self._val.limit_denominator(max_denominator)
return f.numerator, f.denominator return f.numerator, f.denominator
@ -396,14 +406,15 @@ class IFDRational(Rational):
val = float(val) val = float(val)
return val == other return val == other
def __getstate__(self): def __getstate__(self) -> list[float | Fraction]:
return [self._val, self._numerator, self._denominator] return [self._val, self._numerator, self._denominator]
def __setstate__(self, state): def __setstate__(self, state: list[float | Fraction]) -> None:
IFDRational.__init__(self, 0) IFDRational.__init__(self, 0)
_val, _numerator, _denominator = state _val, _numerator, _denominator = state
self._val = _val self._val = _val
self._numerator = _numerator self._numerator = _numerator
assert isinstance(_denominator, int)
self._denominator = _denominator self._denominator = _denominator
""" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
@ -445,8 +456,11 @@ class IFDRational(Rational):
__int__ = _delegate("__int__") __int__ = _delegate("__int__")
def _register_loader(idx: int, size: int): _LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any]
def decorator(func):
def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]:
def decorator(func: _LoaderFunc) -> _LoaderFunc:
from .TiffTags import TYPES from .TiffTags import TYPES
if func.__name__.startswith("load_"): if func.__name__.startswith("load_"):
@ -471,12 +485,13 @@ def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None:
idx, fmt, name = idx_fmt_name idx, fmt, name = idx_fmt_name
TYPES[idx] = name TYPES[idx] = name
size = struct.calcsize(f"={fmt}") size = struct.calcsize(f"={fmt}")
_load_dispatch[idx] = ( # noqa: F821
size, def basic_handler(
lambda self, data, legacy_api=True: ( self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True
self._unpack(f"{len(data) // size}{fmt}", data) ) -> tuple[Any, ...]:
), return self._unpack(f"{len(data) // size}{fmt}", data)
)
_load_dispatch[idx] = size, basic_handler # noqa: F821
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
b"".join(self._pack(fmt, value) for value in values) b"".join(self._pack(fmt, value) for value in values)
) )
@ -549,7 +564,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
""" """
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {}
_write_dispatch: dict[int, Callable[..., Any]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {}
def __init__( def __init__(
@ -609,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tagdata: dict[int, bytes] = {} self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None self._next = None
self._offset = None self._offset: int | None = None
def __str__(self) -> str: def __str__(self) -> str:
return str(dict(self)) return str(dict(self))
def named(self): def named(self) -> dict[str, Any]:
""" """
:returns: dict of name|key: value :returns: dict of name|key: value
@ -628,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __len__(self) -> int: def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2)) return len(set(self._tagdata) | set(self._tags_v2))
def __getitem__(self, tag): def __getitem__(self, tag: int) -> Any:
if tag not in self._tags_v2: # unpack on the fly if tag not in self._tags_v2: # unpack on the fly
data = self._tagdata[tag] data = self._tagdata[tag]
typ = self.tagtype[tag] typ = self.tagtype[tag]
@ -642,10 +657,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __contains__(self, tag: object) -> bool: def __contains__(self, tag: object) -> bool:
return tag in self._tags_v2 or tag in self._tagdata return tag in self._tags_v2 or tag in self._tagdata
def __setitem__(self, tag: int, value) -> None: def __setitem__(self, tag: int, value: Any) -> None:
self._setitem(tag, value, self.legacy_api) self._setitem(tag, value, self.legacy_api)
def _setitem(self, tag: int, value, legacy_api: bool) -> None: def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None:
basetypes = (Number, bytes, str) basetypes = (Number, bytes, str)
info = TiffTags.lookup(tag, self.group) info = TiffTags.lookup(tag, self.group)
@ -730,13 +745,13 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tags_v1.pop(tag, None) self._tags_v1.pop(tag, None)
self._tagdata.pop(tag, None) self._tagdata.pop(tag, None)
def __iter__(self): def __iter__(self) -> Iterator[int]:
return iter(set(self._tagdata) | set(self._tags_v2)) return iter(set(self._tagdata) | set(self._tags_v2))
def _unpack(self, fmt: str, data: bytes): def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]:
return struct.unpack(self._endian + fmt, data) return struct.unpack(self._endian + fmt, data)
def _pack(self, fmt: str, *values): def _pack(self, fmt: str, *values: Any) -> bytes:
return struct.pack(self._endian + fmt, *values) return struct.pack(self._endian + fmt, *values)
list( list(
@ -784,16 +799,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
return value + b"\0" return value + b"\0"
@_register_loader(5, 8) @_register_loader(5, 8)
def load_rational(self, data, legacy_api: bool = True): def load_rational(
self, data: bytes, legacy_api: bool = True
) -> tuple[tuple[int, int] | IFDRational, ...]:
vals = self._unpack(f"{len(data) // 4}L", data) vals = self._unpack(f"{len(data) // 4}L", data)
def combine(a, b): def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
return (a, b) if legacy_api else IFDRational(a, b) return (a, b) if legacy_api else IFDRational(a, b)
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@_register_writer(5) @_register_writer(5)
def write_rational(self, *values) -> bytes: def write_rational(self, *values: IFDRational) -> bytes:
return b"".join( return b"".join(
self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
) )
@ -811,16 +828,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
return value return value
@_register_loader(10, 8) @_register_loader(10, 8)
def load_signed_rational(self, data: bytes, legacy_api: bool = True): def load_signed_rational(
self, data: bytes, legacy_api: bool = True
) -> tuple[tuple[int, int] | IFDRational, ...]:
vals = self._unpack(f"{len(data) // 4}l", data) vals = self._unpack(f"{len(data) // 4}l", data)
def combine(a, b): def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
return (a, b) if legacy_api else IFDRational(a, b) return (a, b) if legacy_api else IFDRational(a, b)
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@_register_writer(10) @_register_writer(10)
def write_signed_rational(self, *values) -> bytes: def write_signed_rational(self, *values: IFDRational) -> bytes:
return b"".join( return b"".join(
self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
for frac in values for frac in values
@ -836,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise OSError(msg) raise OSError(msg)
return ret return ret
def load(self, fp): def load(self, fp: IO[bytes]) -> None:
self.reset() self.reset()
self._offset = fp.tell() self._offset = fp.tell()
@ -903,11 +922,11 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg)) warnings.warn(str(msg))
return return
def tobytes(self, offset=0): def tobytes(self, offset: int = 0) -> bytes:
# FIXME What about tagdata? # FIXME What about tagdata?
result = self._pack("H", len(self._tags_v2)) result = self._pack("H", len(self._tags_v2))
entries = [] entries: list[tuple[int, int, int, bytes, bytes]] = []
offset = offset + len(result) + len(self._tags_v2) * 12 + 4 offset = offset + len(result) + len(self._tags_v2) * 12 + 4
stripoffsets = None stripoffsets = None
@ -916,7 +935,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
for tag, value in sorted(self._tags_v2.items()): for tag, value in sorted(self._tags_v2.items()):
if tag == STRIPOFFSETS: if tag == STRIPOFFSETS:
stripoffsets = len(entries) stripoffsets = len(entries)
typ = self.tagtype.get(tag) typ = self.tagtype[tag]
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict) is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd: if is_ifd:
@ -1072,14 +1091,14 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __len__(self) -> int: def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v1)) return len(set(self._tagdata) | set(self._tags_v1))
def __iter__(self): def __iter__(self) -> Iterator[int]:
return iter(set(self._tagdata) | set(self._tags_v1)) return iter(set(self._tagdata) | set(self._tags_v1))
def __setitem__(self, tag: int, value) -> None: def __setitem__(self, tag: int, value: Any) -> None:
for legacy_api in (False, True): for legacy_api in (False, True):
self._setitem(tag, value, legacy_api) self._setitem(tag, value, legacy_api)
def __getitem__(self, tag): def __getitem__(self, tag: int) -> Any:
if tag not in self._tags_v1: # unpack on the fly if tag not in self._tags_v1: # unpack on the fly
data = self._tagdata[tag] data = self._tagdata[tag]
typ = self.tagtype[tag] typ = self.tagtype[tag]
@ -1105,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile):
format_description = "Adobe TIFF" format_description = "Adobe TIFF"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def __init__(self, fp=None, filename=None): def __init__(
self.tag_v2 = None self,
fp: StrOrBytesPath | IO[bytes] | None = None,
filename: str | bytes | None = None,
) -> None:
self.tag_v2: ImageFileDirectory_v2
""" Image file directory (tag dictionary) """ """ Image file directory (tag dictionary) """
self.tag = None self.tag: ImageFileDirectory_v1
""" Legacy tag entries """ """ Legacy tag entries """
super().__init__(fp, filename) super().__init__(fp, filename)
@ -1124,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile):
self.tag_v2 = ImageFileDirectory_v2(ifh) self.tag_v2 = ImageFileDirectory_v2(ifh)
# legacy IFD entries will be filled in later
self.ifd: ImageFileDirectory_v1 | None = None
# setup frame pointers # setup frame pointers
self.__first = self.__next = self.tag_v2.next self.__first = self.__next = self.tag_v2.next
self.__frame = -1 self.__frame = -1
@ -1374,11 +1394,14 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("- photometric_interpretation: %s", photo) logger.debug("- photometric_interpretation: %s", photo)
logger.debug("- planar_configuration: %s", self._planar_configuration) logger.debug("- planar_configuration: %s", self._planar_configuration)
logger.debug("- fill_order: %s", fillorder) logger.debug("- fill_order: %s", fillorder)
logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
# size # size
xsize = int(self.tag_v2.get(IMAGEWIDTH)) xsize = self.tag_v2.get(IMAGEWIDTH)
ysize = int(self.tag_v2.get(IMAGELENGTH)) ysize = self.tag_v2.get(IMAGELENGTH)
if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions"
raise ValueError(msg)
self._size = xsize, ysize self._size = xsize, ysize
logger.debug("- size: %s", self.size) logger.debug("- size: %s", self.size)
@ -1526,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile):
else: else:
# tiled image # tiled image
offsets = self.tag_v2[TILEOFFSETS] offsets = self.tag_v2[TILEOFFSETS]
w = self.tag_v2.get(TILEWIDTH) tilewidth = self.tag_v2.get(TILEWIDTH)
h = self.tag_v2.get(TILELENGTH) h = self.tag_v2.get(TILELENGTH)
if not isinstance(tilewidth, int) or not isinstance(h, int):
msg = "Invalid tile dimensions"
raise ValueError(msg)
w = tilewidth
for offset in offsets: for offset in offsets:
if x + w > xsize: if x + w > xsize:
@ -1605,7 +1632,7 @@ SAVE_INFO = {
} }
def _save(im, fp, filename): def _save(im: Image.Image, fp, filename: str | bytes) -> None:
try: try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e: except KeyError as e:
@ -1741,10 +1768,11 @@ def _save(im, fp, filename):
if im.mode == "1": if im.mode == "1":
inverted_im = im.copy() inverted_im = im.copy()
px = inverted_im.load() px = inverted_im.load()
for y in range(inverted_im.height): if px is not None:
for x in range(inverted_im.width): for y in range(inverted_im.height):
px[x, y] = 0 if px[x, y] == 255 else 255 for x in range(inverted_im.width):
im = inverted_im px[x, y] = 0 if px[x, y] == 255 else 255
im = inverted_im
else: else:
im = ImageOps.invert(im) im = ImageOps.invert(im)
@ -1786,11 +1814,11 @@ def _save(im, fp, filename):
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
if im.mode == "YCbCr": if im.mode == "YCbCr":
for tag, value in { for tag, default_value in {
YCBCRSUBSAMPLING: (1, 1), YCBCRSUBSAMPLING: (1, 1),
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
}.items(): }.items():
ifd.setdefault(tag, value) ifd.setdefault(tag, default_value)
blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
if libtiff: if libtiff:
@ -1833,7 +1861,7 @@ def _save(im, fp, filename):
] ]
# bits per sample is a single short in the tiff directory, not a list. # bits per sample is a single short in the tiff directory, not a list.
atts = {BITSPERSAMPLE: bits[0]} atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
# Merge the ones that we have with (optional) more bits from # Merge the ones that we have with (optional) more bits from
# the original file, e.g x,y resolution so that we can # the original file, e.g x,y resolution so that we can
# save(load('')) == original file. # save(load('')) == original file.
@ -1904,13 +1932,15 @@ def _save(im, fp, filename):
offset = ifd.save(fp) offset = ifd.save(fp)
ImageFile._save( ImageFile._save(
im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] im,
fp,
[ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
) )
# -- helper for multi-page save -- # -- helper for multi-page save --
if "_debug_multipage" in encoderinfo: if "_debug_multipage" in encoderinfo:
# just to access o32 and o16 (using correct byte order) # just to access o32 and o16 (using correct byte order)
im._debug_multipage = ifd setattr(im, "_debug_multipage", ifd)
class AppendingTiffWriter: class AppendingTiffWriter:
@ -1943,17 +1973,18 @@ class AppendingTiffWriter:
521, # JPEGACTables 521, # JPEGACTables
} }
def __init__(self, fn, new: bool = False) -> None: def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None:
if hasattr(fn, "read"): self.f: IO[bytes]
self.f = fn if is_path(fn):
self.close_fp = False
else:
self.name = fn self.name = fn
self.close_fp = True self.close_fp = True
try: try:
self.f = open(fn, "w+b" if new else "r+b") self.f = open(fn, "w+b" if new else "r+b")
except OSError: except OSError:
self.f = open(fn, "w+b") self.f = open(fn, "w+b")
else:
self.f = cast(IO[bytes], fn)
self.close_fp = False
self.beginning = self.f.tell() self.beginning = self.f.tell()
self.setup() self.setup()
@ -1961,7 +1992,7 @@ class AppendingTiffWriter:
# Reset everything. # Reset everything.
self.f.seek(self.beginning, os.SEEK_SET) self.f.seek(self.beginning, os.SEEK_SET)
self.whereToWriteNewIFDOffset = None self.whereToWriteNewIFDOffset: int | None = None
self.offsetOfNewPage = 0 self.offsetOfNewPage = 0
self.IIMM = iimm = self.f.read(4) self.IIMM = iimm = self.f.read(4)
@ -2000,6 +2031,7 @@ class AppendingTiffWriter:
ifd_offset = self.readLong() ifd_offset = self.readLong()
ifd_offset += self.offsetOfNewPage ifd_offset += self.offsetOfNewPage
assert self.whereToWriteNewIFDOffset is not None
self.f.seek(self.whereToWriteNewIFDOffset) self.f.seek(self.whereToWriteNewIFDOffset)
self.writeLong(ifd_offset) self.writeLong(ifd_offset)
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
@ -2020,7 +2052,7 @@ class AppendingTiffWriter:
def tell(self) -> int: def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage return self.f.tell() - self.offsetOfNewPage
def seek(self, offset: int, whence=io.SEEK_SET) -> int: def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
if whence == os.SEEK_SET: if whence == os.SEEK_SET:
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
@ -2065,38 +2097,34 @@ class AppendingTiffWriter:
(value,) = struct.unpack(self.longFmt, self.f.read(4)) (value,) = struct.unpack(self.longFmt, self.f.read(4))
return value return value
@staticmethod
def _verify_bytes_written(bytes_written: int | None, expected: int) -> None:
if bytes_written is not None and bytes_written != expected:
msg = f"wrote only {bytes_written} bytes but wanted {expected}"
raise RuntimeError(msg)
def rewriteLastShortToLong(self, value: int) -> None: def rewriteLastShortToLong(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: self._verify_bytes_written(bytes_written, 4)
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
def rewriteLastShort(self, value: int) -> None: def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: self._verify_bytes_written(bytes_written, 2)
msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg)
def rewriteLastLong(self, value: int) -> None: def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR) self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: self._verify_bytes_written(bytes_written, 4)
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
def writeShort(self, value: int) -> None: def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: self._verify_bytes_written(bytes_written, 2)
msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg)
def writeLong(self, value: int) -> None: def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: self._verify_bytes_written(bytes_written, 4)
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
def close(self) -> None: def close(self) -> None:
self.finalize() self.finalize()
@ -2111,7 +2139,6 @@ class AppendingTiffWriter:
field_size = self.fieldSizes[field_type] field_size = self.fieldSizes[field_type]
total_size = field_size * count total_size = field_size * count
is_local = total_size <= 4 is_local = total_size <= 4
offset: int | None
if not is_local: if not is_local:
offset = self.readLong() + self.offsetOfNewPage offset = self.readLong() + self.offsetOfNewPage
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
@ -2131,8 +2158,6 @@ class AppendingTiffWriter:
) )
self.f.seek(cur_pos) self.f.seek(cur_pos)
offset = cur_pos = None
elif is_local: elif is_local:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)

View File

@ -45,22 +45,6 @@ class WebPImageFile(ImageFile.ImageFile):
__logical_frame = 0 __logical_frame = 0
def _open(self) -> None: def _open(self) -> None:
if not _webp.HAVE_WEBPANIM:
# Legacy mode
data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode(
self.fp.read()
)
if icc_profile:
self.info["icc_profile"] = icc_profile
if exif:
self.info["exif"] = exif
self._size = width, height
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.mode)]
self.n_frames = 1
self.is_animated = False
return
# Use the newer AnimDecoder API to parse the (possibly) animated file, # Use the newer AnimDecoder API to parse the (possibly) animated file,
# and access muxed chunks like ICC/EXIF/XMP. # and access muxed chunks like ICC/EXIF/XMP.
self._decoder = _webp.WebPAnimDecoder(self.fp.read()) self._decoder = _webp.WebPAnimDecoder(self.fp.read())
@ -145,21 +129,20 @@ class WebPImageFile(ImageFile.ImageFile):
self._get_next() # Advance to the requested frame self._get_next() # Advance to the requested frame
def load(self) -> Image.core.PixelAccess | None: def load(self) -> Image.core.PixelAccess | None:
if _webp.HAVE_WEBPANIM: if self.__loaded != self.__logical_frame:
if self.__loaded != self.__logical_frame: self._seek(self.__logical_frame)
self._seek(self.__logical_frame)
# We need to load the image data for this frame # We need to load the image data for this frame
data, timestamp, duration = self._get_next() data, timestamp, duration = self._get_next()
self.info["timestamp"] = timestamp self.info["timestamp"] = timestamp
self.info["duration"] = duration self.info["duration"] = duration
self.__loaded = self.__logical_frame self.__loaded = self.__logical_frame
# Set tile # Set tile
if self.fp and self._exclusive_fp: if self.fp and self._exclusive_fp:
self.fp.close() self.fp.close()
self.fp = BytesIO(data) self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
return super().load() return super().load()
@ -167,9 +150,6 @@ class WebPImageFile(ImageFile.ImageFile):
pass pass
def tell(self) -> int: def tell(self) -> int:
if not _webp.HAVE_WEBPANIM:
return super().tell()
return self.__logical_frame return self.__logical_frame
@ -357,7 +337,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
Image.register_open(WebPImageFile.format, WebPImageFile, _accept) Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
if SUPPORTED: if SUPPORTED:
Image.register_save(WebPImageFile.format, _save) Image.register_save(WebPImageFile.format, _save)
if _webp.HAVE_WEBPANIM: Image.register_save_all(WebPImageFile.format, _save_all)
Image.register_save_all(WebPImageFile.format, _save_all)
Image.register_extension(WebPImageFile.format, ".webp") Image.register_extension(WebPImageFile.format, ".webp")
Image.register_mime(WebPImageFile.format, "image/webp") Image.register_mime(WebPImageFile.format, "image/webp")

View File

@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(b"static char im_bits[] = {\n") fp.write(b"static char im_bits[] = {\n")
ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)])
fp.write(b"};\n") fp.write(b"};\n")

View File

@ -9,6 +9,7 @@ from typing import IO
import PIL import PIL
from . import Image from . import Image
from ._deprecate import deprecate
modules = { modules = {
"pil": ("PIL._imaging", "PILLOW_VERSION"), "pil": ("PIL._imaging", "PILLOW_VERSION"),
@ -118,10 +119,10 @@ def get_supported_codecs() -> list[str]:
return [f for f in codecs if check_codec(f)] return [f for f in codecs if check_codec(f)]
features = { features: dict[str, tuple[str, str | bool, str | None]] = {
"webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), "webp_anim": ("PIL._webp", True, None),
"webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), "webp_mux": ("PIL._webp", True, None),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), "transp_webp": ("PIL._webp", True, None),
"raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"),
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
@ -147,6 +148,9 @@ def check_feature(feature: str) -> bool | None:
try: try:
imported_module = __import__(module, fromlist=["PIL"]) imported_module = __import__(module, fromlist=["PIL"])
if isinstance(flag, bool):
deprecate(f'check_feature("{feature}")', 12)
return flag
return getattr(imported_module, flag) return getattr(imported_module, flag)
except ModuleNotFoundError: except ModuleNotFoundError:
return None return None
@ -176,7 +180,17 @@ def get_supported_features() -> list[str]:
""" """
:returns: A list of all supported features. :returns: A list of all supported features.
""" """
return [f for f in features if check_feature(f)] supported_features = []
for f, (module, flag, _) in features.items():
if flag is True:
for feature, (feature_module, _) in modules.items():
if feature_module == module:
if check_module(feature):
supported_features.append(f)
break
elif check_feature(f):
supported_features.append(f)
return supported_features
def check(feature: str) -> bool | None: def check(feature: str) -> bool | None:
@ -271,9 +285,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
("freetype2", "FREETYPE2"), ("freetype2", "FREETYPE2"),
("littlecms2", "LITTLECMS2"), ("littlecms2", "LITTLECMS2"),
("webp", "WEBP"), ("webp", "WEBP"),
("transp_webp", "WEBP Transparency"),
("webp_mux", "WEBPMUX"),
("webp_anim", "WEBP Animation"),
("jpg", "JPEG"), ("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"), ("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"), ("zlib", "ZLIB (PNG/ZIP)"),

View File

@ -92,19 +92,7 @@
#define _USE_MATH_DEFINES #define _USE_MATH_DEFINES
#include <math.h> #include <math.h>
#include <stddef.h>
/* Configuration stuff. Feel free to undef things you don't need. */
#define WITH_IMAGECHOPS /* ImageChops support */
#define WITH_IMAGEDRAW /* ImageDraw support */
#define WITH_MAPPING /* use memory mapping to read some file formats */
#define WITH_IMAGEPATH /* ImagePath stuff */
#define WITH_ARROW /* arrow graphics stuff (experimental) */
#define WITH_EFFECTS /* special effects */
#define WITH_QUANTIZE /* quantization support */
#define WITH_RANKFILTER /* rank filter */
#define WITH_MODEFILTER /* mode filter */
#define WITH_THREADING /* "friendly" threading support */
#define WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */
#undef VERBOSE #undef VERBOSE
@ -123,8 +111,6 @@ typedef struct {
static PyTypeObject Imaging_Type; static PyTypeObject Imaging_Type;
#ifdef WITH_IMAGEDRAW
typedef struct { typedef struct {
/* to write a character, cut out sxy from glyph data, place /* to write a character, cut out sxy from glyph data, place
at current position plus dxy, and advance by (dx, dy) */ at current position plus dxy, and advance by (dx, dy) */
@ -151,8 +137,6 @@ typedef struct {
static PyTypeObject ImagingDraw_Type; static PyTypeObject ImagingDraw_Type;
#endif
typedef struct { typedef struct {
PyObject_HEAD ImagingObject *image; PyObject_HEAD ImagingObject *image;
int readonly; int readonly;
@ -215,16 +199,12 @@ PyImaging_AsImaging(PyObject *op) {
void void
ImagingSectionEnter(ImagingSectionCookie *cookie) { ImagingSectionEnter(ImagingSectionCookie *cookie) {
#ifdef WITH_THREADING
*cookie = (PyThreadState *)PyEval_SaveThread(); *cookie = (PyThreadState *)PyEval_SaveThread();
#endif
} }
void void
ImagingSectionLeave(ImagingSectionCookie *cookie) { ImagingSectionLeave(ImagingSectionCookie *cookie) {
#ifdef WITH_THREADING
PyEval_RestoreThread((PyThreadState *)*cookie); PyEval_RestoreThread((PyThreadState *)*cookie);
#endif
} }
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
@ -1091,7 +1071,6 @@ _filter(ImagingObject *self, PyObject *args) {
return imOut; return imOut;
} }
#ifdef WITH_UNSHARPMASK
static PyObject * static PyObject *
_gaussian_blur(ImagingObject *self, PyObject *args) { _gaussian_blur(ImagingObject *self, PyObject *args) {
Imaging imIn; Imaging imIn;
@ -1116,7 +1095,6 @@ _gaussian_blur(ImagingObject *self, PyObject *args) {
return PyImagingNew(imOut); return PyImagingNew(imOut);
} }
#endif
static PyObject * static PyObject *
_getpalette(ImagingObject *self, PyObject *args) { _getpalette(ImagingObject *self, PyObject *args) {
@ -1374,7 +1352,6 @@ _entropy(ImagingObject *self, PyObject *args) {
return PyFloat_FromDouble(-entropy); return PyFloat_FromDouble(-entropy);
} }
#ifdef WITH_MODEFILTER
static PyObject * static PyObject *
_modefilter(ImagingObject *self, PyObject *args) { _modefilter(ImagingObject *self, PyObject *args) {
int size; int size;
@ -1384,7 +1361,6 @@ _modefilter(ImagingObject *self, PyObject *args) {
return PyImagingNew(ImagingModeFilter(self->image, size)); return PyImagingNew(ImagingModeFilter(self->image, size));
} }
#endif
static PyObject * static PyObject *
_offset(ImagingObject *self, PyObject *args) { _offset(ImagingObject *self, PyObject *args) {
@ -1716,8 +1692,6 @@ _putdata(ImagingObject *self, PyObject *args) {
return Py_None; return Py_None;
} }
#ifdef WITH_QUANTIZE
static PyObject * static PyObject *
_quantize(ImagingObject *self, PyObject *args) { _quantize(ImagingObject *self, PyObject *args) {
int colours = 256; int colours = 256;
@ -1734,7 +1708,6 @@ _quantize(ImagingObject *self, PyObject *args) {
return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans));
} }
#endif
static PyObject * static PyObject *
_putpalette(ImagingObject *self, PyObject *args) { _putpalette(ImagingObject *self, PyObject *args) {
@ -1870,7 +1843,6 @@ _putpixel(ImagingObject *self, PyObject *args) {
return Py_None; return Py_None;
} }
#ifdef WITH_RANKFILTER
static PyObject * static PyObject *
_rankfilter(ImagingObject *self, PyObject *args) { _rankfilter(ImagingObject *self, PyObject *args) {
int size, rank; int size, rank;
@ -1880,7 +1852,6 @@ _rankfilter(ImagingObject *self, PyObject *args) {
return PyImagingNew(ImagingRankFilter(self->image, size, rank)); return PyImagingNew(ImagingRankFilter(self->image, size, rank));
} }
#endif
static PyObject * static PyObject *
_resize(ImagingObject *self, PyObject *args) { _resize(ImagingObject *self, PyObject *args) {
@ -2162,7 +2133,6 @@ _transpose(ImagingObject *self, PyObject *args) {
return PyImagingNew(imOut); return PyImagingNew(imOut);
} }
#ifdef WITH_UNSHARPMASK
static PyObject * static PyObject *
_unsharp_mask(ImagingObject *self, PyObject *args) { _unsharp_mask(ImagingObject *self, PyObject *args) {
Imaging imIn; Imaging imIn;
@ -2186,7 +2156,6 @@ _unsharp_mask(ImagingObject *self, PyObject *args) {
return PyImagingNew(imOut); return PyImagingNew(imOut);
} }
#endif
static PyObject * static PyObject *
_box_blur(ImagingObject *self, PyObject *args) { _box_blur(ImagingObject *self, PyObject *args) {
@ -2463,9 +2432,7 @@ _split(ImagingObject *self) {
return list; return list;
} }
/* -------------------------------------------------------------------- */ /* Channel operations (ImageChops) ------------------------------------ */
#ifdef WITH_IMAGECHOPS
static PyObject * static PyObject *
_chop_invert(ImagingObject *self) { _chop_invert(ImagingObject *self) {
@ -2646,11 +2613,8 @@ _chop_overlay(ImagingObject *self, PyObject *args) {
return PyImagingNew(ImagingOverlay(self->image, imagep->image)); return PyImagingNew(ImagingOverlay(self->image, imagep->image));
} }
#endif
/* -------------------------------------------------------------------- */ /* Fonts (ImageDraw and ImageFont) ------------------------------------ */
#ifdef WITH_IMAGEDRAW
static PyObject * static PyObject *
_font_new(PyObject *self_, PyObject *args) { _font_new(PyObject *self_, PyObject *args) {
@ -2879,7 +2843,7 @@ static struct PyMethodDef _font_methods[] = {
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };
/* -------------------------------------------------------------------- */ /* Graphics (ImageDraw) ----------------------------------------------- */
static PyObject * static PyObject *
_draw_new(PyObject *self_, PyObject *args) { _draw_new(PyObject *self_, PyObject *args) {
@ -3233,8 +3197,6 @@ _draw_points(ImagingDrawObject *self, PyObject *args) {
return Py_None; return Py_None;
} }
#ifdef WITH_ARROW
/* from outline.c */ /* from outline.c */
extern ImagingOutline extern ImagingOutline
PyOutline_AsOutline(PyObject *outline); PyOutline_AsOutline(PyObject *outline);
@ -3264,8 +3226,6 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) {
return Py_None; return Py_None;
} }
#endif
static PyObject * static PyObject *
_draw_pieslice(ImagingDrawObject *self, PyObject *args) { _draw_pieslice(ImagingDrawObject *self, PyObject *args) {
double *xy; double *xy;
@ -3431,12 +3391,9 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) {
} }
static struct PyMethodDef _draw_methods[] = { static struct PyMethodDef _draw_methods[] = {
#ifdef WITH_IMAGEDRAW
/* Graphics (ImageDraw) */ /* Graphics (ImageDraw) */
{"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS},
#ifdef WITH_ARROW
{"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS},
#endif
{"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS},
{"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS},
{"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, {"draw_points", (PyCFunction)_draw_points, METH_VARARGS},
@ -3446,12 +3403,9 @@ static struct PyMethodDef _draw_methods[] = {
{"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS},
{"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS},
{"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS},
#endif
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };
#endif
static PyObject * static PyObject *
pixel_access_new(ImagingObject *imagep, PyObject *args) { pixel_access_new(ImagingObject *imagep, PyObject *args) {
PixelAccessObject *self; PixelAccessObject *self;
@ -3532,11 +3486,9 @@ pixel_access_setitem(PixelAccessObject *self, PyObject *xy, PyObject *color) {
} }
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* EFFECTS (experimental) */ /* EFFECTS (experimental) */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
#ifdef WITH_EFFECTS
static PyObject * static PyObject *
_effect_mandelbrot(ImagingObject *self, PyObject *args) { _effect_mandelbrot(ImagingObject *self, PyObject *args) {
int xsize = 512; int xsize = 512;
@ -3588,8 +3540,6 @@ _effect_spread(ImagingObject *self, PyObject *args) {
return PyImagingNew(ImagingEffectSpread(self->image, dist)); return PyImagingNew(ImagingEffectSpread(self->image, dist));
} }
#endif
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* UTILITIES */ /* UTILITIES */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
@ -3670,20 +3620,14 @@ static struct PyMethodDef methods[] = {
{"filter", (PyCFunction)_filter, METH_VARARGS}, {"filter", (PyCFunction)_filter, METH_VARARGS},
{"histogram", (PyCFunction)_histogram, METH_VARARGS}, {"histogram", (PyCFunction)_histogram, METH_VARARGS},
{"entropy", (PyCFunction)_entropy, METH_VARARGS}, {"entropy", (PyCFunction)_entropy, METH_VARARGS},
#ifdef WITH_MODEFILTER
{"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, {"modefilter", (PyCFunction)_modefilter, METH_VARARGS},
#endif
{"offset", (PyCFunction)_offset, METH_VARARGS}, {"offset", (PyCFunction)_offset, METH_VARARGS},
{"paste", (PyCFunction)_paste, METH_VARARGS}, {"paste", (PyCFunction)_paste, METH_VARARGS},
{"point", (PyCFunction)_point, METH_VARARGS}, {"point", (PyCFunction)_point, METH_VARARGS},
{"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, {"point_transform", (PyCFunction)_point_transform, METH_VARARGS},
{"putdata", (PyCFunction)_putdata, METH_VARARGS}, {"putdata", (PyCFunction)_putdata, METH_VARARGS},
#ifdef WITH_QUANTIZE
{"quantize", (PyCFunction)_quantize, METH_VARARGS}, {"quantize", (PyCFunction)_quantize, METH_VARARGS},
#endif
#ifdef WITH_RANKFILTER
{"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS},
#endif
{"resize", (PyCFunction)_resize, METH_VARARGS}, {"resize", (PyCFunction)_resize, METH_VARARGS},
{"reduce", (PyCFunction)_reduce, METH_VARARGS}, {"reduce", (PyCFunction)_reduce, METH_VARARGS},
{"transpose", (PyCFunction)_transpose, METH_VARARGS}, {"transpose", (PyCFunction)_transpose, METH_VARARGS},
@ -3709,7 +3653,6 @@ static struct PyMethodDef methods[] = {
{"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS},
{"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS},
#ifdef WITH_IMAGECHOPS
/* Channel operations (ImageChops) */ /* Channel operations (ImageChops) */
{"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS},
{"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS},
@ -3728,20 +3671,14 @@ static struct PyMethodDef methods[] = {
{"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS},
{"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS},
#endif /* Unsharpmask extension */
#ifdef WITH_UNSHARPMASK
/* Kevin Cazabon's unsharpmask extension */
{"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS},
{"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS},
#endif
{"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, {"box_blur", (PyCFunction)_box_blur, METH_VARARGS},
#ifdef WITH_EFFECTS
/* Special effects */ /* Special effects */
{"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS},
#endif
/* Misc. */ /* Misc. */
{"new_block", (PyCFunction)_new_block, METH_VARARGS}, {"new_block", (PyCFunction)_new_block, METH_VARARGS},
@ -3870,8 +3807,6 @@ static PyTypeObject Imaging_Type = {
getsetters, /*tp_getset*/ getsetters, /*tp_getset*/
}; };
#ifdef WITH_IMAGEDRAW
static PyTypeObject ImagingFont_Type = { static PyTypeObject ImagingFont_Type = {
PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/
sizeof(ImagingFontObject), /*tp_basicsize*/ sizeof(ImagingFontObject), /*tp_basicsize*/
@ -3938,8 +3873,6 @@ static PyTypeObject ImagingDraw_Type = {
0, /*tp_getset*/ 0, /*tp_getset*/
}; };
#endif
static PyMappingMethods pixel_access_as_mapping = { static PyMappingMethods pixel_access_as_mapping = {
(lenfunc)NULL, /*mp_length*/ (lenfunc)NULL, /*mp_length*/
(binaryfunc)pixel_access_getitem, /*mp_subscript*/ (binaryfunc)pixel_access_getitem, /*mp_subscript*/
@ -3971,7 +3904,6 @@ static PyObject *
_get_stats(PyObject *self, PyObject *args) { _get_stats(PyObject *self, PyObject *args) {
PyObject *d; PyObject *d;
PyObject *v; PyObject *v;
ImagingMemoryArena arena = &ImagingDefaultArena;
if (!PyArg_ParseTuple(args, ":get_stats")) { if (!PyArg_ParseTuple(args, ":get_stats")) {
return NULL; return NULL;
@ -3981,6 +3913,10 @@ _get_stats(PyObject *self, PyObject *args) {
if (!d) { if (!d) {
return NULL; return NULL;
} }
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingMemoryArena arena = &ImagingDefaultArena;
v = PyLong_FromLong(arena->stats_new_count); v = PyLong_FromLong(arena->stats_new_count);
PyDict_SetItemString(d, "new_count", v ? v : Py_None); PyDict_SetItemString(d, "new_count", v ? v : Py_None);
Py_XDECREF(v); Py_XDECREF(v);
@ -4004,22 +3940,25 @@ _get_stats(PyObject *self, PyObject *args) {
v = PyLong_FromLong(arena->blocks_cached); v = PyLong_FromLong(arena->blocks_cached);
PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None);
Py_XDECREF(v); Py_XDECREF(v);
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
return d; return d;
} }
static PyObject * static PyObject *
_reset_stats(PyObject *self, PyObject *args) { _reset_stats(PyObject *self, PyObject *args) {
ImagingMemoryArena arena = &ImagingDefaultArena;
if (!PyArg_ParseTuple(args, ":reset_stats")) { if (!PyArg_ParseTuple(args, ":reset_stats")) {
return NULL; return NULL;
} }
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingMemoryArena arena = &ImagingDefaultArena;
arena->stats_new_count = 0; arena->stats_new_count = 0;
arena->stats_allocated_blocks = 0; arena->stats_allocated_blocks = 0;
arena->stats_reused_blocks = 0; arena->stats_reused_blocks = 0;
arena->stats_reallocated_blocks = 0; arena->stats_reallocated_blocks = 0;
arena->stats_freed_blocks = 0; arena->stats_freed_blocks = 0;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
@ -4031,7 +3970,10 @@ _get_alignment(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
return PyLong_FromLong(ImagingDefaultArena.alignment); MUTEX_LOCK(&ImagingDefaultArena.mutex);
int alignment = ImagingDefaultArena.alignment;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
return PyLong_FromLong(alignment);
} }
static PyObject * static PyObject *
@ -4040,7 +3982,10 @@ _get_block_size(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
return PyLong_FromLong(ImagingDefaultArena.block_size); MUTEX_LOCK(&ImagingDefaultArena.mutex);
int block_size = ImagingDefaultArena.block_size;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
return PyLong_FromLong(block_size);
} }
static PyObject * static PyObject *
@ -4049,7 +3994,10 @@ _get_blocks_max(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
return PyLong_FromLong(ImagingDefaultArena.blocks_max); MUTEX_LOCK(&ImagingDefaultArena.mutex);
int blocks_max = ImagingDefaultArena.blocks_max;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
return PyLong_FromLong(blocks_max);
} }
static PyObject * static PyObject *
@ -4069,7 +4017,9 @@ _set_alignment(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingDefaultArena.alignment = alignment; ImagingDefaultArena.alignment = alignment;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
@ -4092,7 +4042,9 @@ _set_block_size(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingDefaultArena.block_size = block_size; ImagingDefaultArena.block_size = block_size;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
@ -4108,13 +4060,18 @@ _set_blocks_max(PyObject *self, PyObject *args) {
if (blocks_max < 0) { if (blocks_max < 0) {
PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0"); PyErr_SetString(PyExc_ValueError, "blocks_max should be greater than 0");
return NULL; return NULL;
} else if ((unsigned long)blocks_max > }
SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) {
if ((unsigned long)blocks_max >
SIZE_MAX / sizeof(ImagingDefaultArena.blocks_pool[0])) {
PyErr_SetString(PyExc_ValueError, "blocks_max is too large"); PyErr_SetString(PyExc_ValueError, "blocks_max is too large");
return NULL; return NULL;
} }
if (!ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max)) { MUTEX_LOCK(&ImagingDefaultArena.mutex);
int status = ImagingMemorySetBlocksMax(&ImagingDefaultArena, blocks_max);
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
if (!status) {
return ImagingError_MemoryError(); return ImagingError_MemoryError();
} }
@ -4130,7 +4087,9 @@ _clear_cache(PyObject *self, PyObject *args) {
return NULL; return NULL;
} }
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingMemoryClearCache(&ImagingDefaultArena, i); ImagingMemoryClearCache(&ImagingDefaultArena, i);
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;
@ -4282,13 +4241,11 @@ static PyMethodDef functions[] = {
{"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS},
#endif #endif
/* Memory mapping */ /* Memory mapping */
#ifdef WITH_MAPPING
{"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS},
#endif
/* Display support */
#ifdef _WIN32 #ifdef _WIN32
/* Display support */
{"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS},
{"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS},
{"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS},
@ -4304,30 +4261,22 @@ static PyMethodDef functions[] = {
/* Utilities */ /* Utilities */
{"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS},
/* Special effects (experimental) */ /* Special effects (experimental) */
#ifdef WITH_EFFECTS
{"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS},
{"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS},
{"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS},
{"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS},
{"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */
#endif
/* Drawing support stuff */ /* Drawing support stuff */
#ifdef WITH_IMAGEDRAW
{"font", (PyCFunction)_font_new, METH_VARARGS}, {"font", (PyCFunction)_font_new, METH_VARARGS},
{"draw", (PyCFunction)_draw_new, METH_VARARGS}, {"draw", (PyCFunction)_draw_new, METH_VARARGS},
#endif
/* Experimental path stuff */ /* Experimental path stuff */
#ifdef WITH_IMAGEPATH
{"path", (PyCFunction)PyPath_Create, METH_VARARGS}, {"path", (PyCFunction)PyPath_Create, METH_VARARGS},
#endif
/* Experimental arrow graphics stuff */ /* Experimental arrow graphics stuff */
#ifdef WITH_ARROW
{"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS},
#endif
/* Resource management */ /* Resource management */
{"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, {"get_stats", (PyCFunction)_get_stats, METH_VARARGS},
@ -4352,16 +4301,12 @@ setup_module(PyObject *m) {
if (PyType_Ready(&Imaging_Type) < 0) { if (PyType_Ready(&Imaging_Type) < 0) {
return -1; return -1;
} }
#ifdef WITH_IMAGEDRAW
if (PyType_Ready(&ImagingFont_Type) < 0) { if (PyType_Ready(&ImagingFont_Type) < 0) {
return -1; return -1;
} }
if (PyType_Ready(&ImagingDraw_Type) < 0) { if (PyType_Ready(&ImagingDraw_Type) < 0) {
return -1; return -1;
} }
#endif
if (PyType_Ready(&PixelAccess_Type) < 0) { if (PyType_Ready(&PixelAccess_Type) < 0) {
return -1; return -1;
} }

View File

@ -1243,7 +1243,7 @@ font_getvarnames(FontObject *self) {
return PyErr_NoMemory(); return PyErr_NoMemory();
} }
for (int i = 0; i < num_namedstyles; i++) { for (unsigned int i = 0; i < num_namedstyles; i++) {
list_names_filled[i] = 0; list_names_filled[i] = 0;
} }

View File

@ -4,8 +4,6 @@
#include <webp/encode.h> #include <webp/encode.h>
#include <webp/decode.h> #include <webp/decode.h>
#include <webp/types.h> #include <webp/types.h>
#ifdef HAVE_WEBPMUX
#include <webp/mux.h> #include <webp/mux.h>
#include <webp/demux.h> #include <webp/demux.h>
@ -13,12 +11,10 @@
* Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and
* WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The
* very early versions had some significant differences, so we require later * very early versions had some significant differences, so we require later
* versions, before enabling animation support. * versions.
*/ */
#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 #if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107
#define HAVE_WEBPANIM #error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag
#endif
#endif #endif
void void
@ -35,8 +31,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie) {
/* WebP Muxer Error Handling */ /* WebP Muxer Error Handling */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPMUX
static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = {
"WEBP_MUX_NOT_FOUND", "WEBP_MUX_NOT_FOUND",
"WEBP_MUX_INVALID_ARGUMENT", "WEBP_MUX_INVALID_ARGUMENT",
@ -89,14 +83,10 @@ HandleMuxError(WebPMuxError err, char *chunk) {
return NULL; return NULL;
} }
#endif
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* WebP Animation Support */ /* WebP Animation Support */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPANIM
// Encoder type // Encoder type
typedef struct { typedef struct {
PyObject_HEAD WebPAnimEncoder *enc; PyObject_HEAD WebPAnimEncoder *enc;
@ -576,8 +566,6 @@ static PyTypeObject WebPAnimDecoder_Type = {
0, /*tp_getset*/ 0, /*tp_getset*/
}; };
#endif
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* Legacy WebP Support */ /* Legacy WebP Support */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
@ -652,10 +640,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
config.quality = quality_factor; config.quality = quality_factor;
config.alpha_quality = alpha_quality_factor; config.alpha_quality = alpha_quality_factor;
config.method = method; config.method = method;
#if WEBP_ENCODER_ABI_VERSION >= 0x0209
// the "exact" flag is only available in libwebp 0.5.0 and later
config.exact = exact; config.exact = exact;
#endif
// Validate the config // Validate the config
if (!WebPValidateConfig(&config)) { if (!WebPValidateConfig(&config)) {
@ -687,19 +672,21 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
WebPPictureFree(&pic); WebPPictureFree(&pic);
if (!ok) { if (!ok) {
PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); int error_code = (&pic)->error_code;
char message[50] = "";
if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) {
sprintf(
message,
": Image size exceeds WebP limit of %d pixels",
WEBP_MAX_DIMENSION
);
}
PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message);
return NULL; return NULL;
} }
output = writer.mem; output = writer.mem;
ret_size = writer.size; ret_size = writer.size;
#ifndef HAVE_WEBPMUX
if (ret_size > 0) {
PyObject *ret = PyBytes_FromStringAndSize((char *)output, ret_size);
free(output);
return ret;
}
#else
{ {
/* I want to truncate the *_size items that get passed into WebP /* I want to truncate the *_size items that get passed into WebP
data. Pypy2.1.0 had some issues where the Py_ssize_t items had data. Pypy2.1.0 had some issues where the Py_ssize_t items had
@ -775,132 +762,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
return ret; return ret;
} }
} }
#endif
Py_RETURN_NONE; Py_RETURN_NONE;
} }
PyObject *
WebPDecode_wrapper(PyObject *self, PyObject *args) {
PyBytesObject *webp_string;
const uint8_t *webp;
Py_ssize_t size;
PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL,
*exif = NULL;
WebPDecoderConfig config;
VP8StatusCode vp8_status_code = VP8_STATUS_OK;
char *mode = "RGB";
if (!PyArg_ParseTuple(args, "S", &webp_string)) {
return NULL;
}
if (!WebPInitDecoderConfig(&config)) {
Py_RETURN_NONE;
}
PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size);
vp8_status_code = WebPGetFeatures(webp, size, &config.input);
if (vp8_status_code == VP8_STATUS_OK) {
// If we don't set it, we don't get alpha.
// Initialized to MODE_RGB
if (config.input.has_alpha) {
config.output.colorspace = MODE_RGBA;
mode = "RGBA";
}
#ifndef HAVE_WEBPMUX
vp8_status_code = WebPDecode(webp, size, &config);
#else
{
int copy_data = 0;
WebPData data = {webp, size};
WebPMuxFrameInfo image;
WebPData icc_profile_data = {0};
WebPData exif_data = {0};
WebPMux *mux = WebPMuxCreate(&data, copy_data);
if (NULL == mux) {
goto end;
}
if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) {
WebPMuxDelete(mux);
goto end;
}
webp = image.bitstream.bytes;
size = image.bitstream.size;
vp8_status_code = WebPDecode(webp, size, &config);
if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) {
icc_profile = PyBytes_FromStringAndSize(
(const char *)icc_profile_data.bytes, icc_profile_data.size
);
}
if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) {
exif = PyBytes_FromStringAndSize(
(const char *)exif_data.bytes, exif_data.size
);
}
WebPDataClear(&image.bitstream);
WebPMuxDelete(mux);
}
#endif
}
if (vp8_status_code != VP8_STATUS_OK) {
goto end;
}
if (config.output.colorspace < MODE_YUV) {
bytes = PyBytes_FromStringAndSize(
(char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size
);
} else {
// Skipping YUV for now. Need Test Images.
// UNDONE -- unclear if we'll ever get here if we set mode_rgb*
bytes = PyBytes_FromStringAndSize(
(char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size
);
}
pymode = PyUnicode_FromString(mode);
ret = Py_BuildValue(
"SiiSSS",
bytes,
config.output.width,
config.output.height,
pymode,
NULL == icc_profile ? Py_None : icc_profile,
NULL == exif ? Py_None : exif
);
end:
WebPFreeDecBuffer(&config.output);
Py_XDECREF(bytes);
Py_XDECREF(pymode);
Py_XDECREF(icc_profile);
Py_XDECREF(exif);
if (Py_None == ret) {
Py_RETURN_NONE;
}
return ret;
}
// Return the decoder's version number, packed in hexadecimal using 8bits for
// each of major/minor/revision. E.g: v2.5.7 is 0x020507.
PyObject *
WebPDecoderVersion_wrapper() {
return Py_BuildValue("i", WebPGetDecoderVersion());
}
// Version as string // Version as string
const char * const char *
WebPDecoderVersion_str(void) { WebPDecoderVersion_str(void) {
@ -916,85 +780,26 @@ WebPDecoderVersion_str(void) {
return version; return version;
} }
/*
* The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well.
* Files that are valid with 0.3 are reported as being invalid.
*/
int
WebPDecoderBuggyAlpha(void) {
return WebPGetDecoderVersion() == 0x0103;
}
PyObject *
WebPDecoderBuggyAlpha_wrapper() {
return Py_BuildValue("i", WebPDecoderBuggyAlpha());
}
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
/* Module Setup */ /* Module Setup */
/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */
static PyMethodDef webpMethods[] = { static PyMethodDef webpMethods[] = {
#ifdef HAVE_WEBPANIM
{"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"},
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
#endif
{"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"},
{"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"},
{"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"},
{"WebPDecoderBuggyAlpha",
WebPDecoderBuggyAlpha_wrapper,
METH_NOARGS,
"WebPDecoderBuggyAlpha"},
{NULL, NULL} {NULL, NULL}
}; };
void
addMuxFlagToModule(PyObject *m) {
PyObject *have_webpmux;
#ifdef HAVE_WEBPMUX
have_webpmux = Py_True;
#else
have_webpmux = Py_False;
#endif
Py_INCREF(have_webpmux);
PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux);
}
void
addAnimFlagToModule(PyObject *m) {
PyObject *have_webpanim;
#ifdef HAVE_WEBPANIM
have_webpanim = Py_True;
#else
have_webpanim = Py_False;
#endif
Py_INCREF(have_webpanim);
PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim);
}
void
addTransparencyFlagToModule(PyObject *m) {
PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha());
if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) {
Py_DECREF(have_transparency);
}
}
static int static int
setup_module(PyObject *m) { setup_module(PyObject *m) {
#ifdef HAVE_WEBPANIM
/* Ready object types */ /* Ready object types */
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||
PyType_Ready(&WebPAnimEncoder_Type) < 0) { PyType_Ready(&WebPAnimEncoder_Type) < 0) {
return -1; return -1;
} }
#endif
PyObject *d = PyModule_GetDict(m);
addMuxFlagToModule(m);
addAnimFlagToModule(m);
addTransparencyFlagToModule(m);
PyObject *d = PyModule_GetDict(m);
PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str());
PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None);
Py_XDECREF(v); Py_XDECREF(v);

View File

@ -161,6 +161,9 @@ typedef struct ImagingMemoryArena {
int stats_reallocated_blocks; /* Number of blocks which were actually reallocated int stats_reallocated_blocks; /* Number of blocks which were actually reallocated
after retrieving */ after retrieving */
int stats_freed_blocks; /* Number of freed blocks */ int stats_freed_blocks; /* Number of freed blocks */
#ifdef Py_GIL_DISABLED
PyMutex mutex;
#endif
} *ImagingMemoryArena; } *ImagingMemoryArena;
/* Objects */ /* Objects */
@ -710,6 +713,15 @@ _imaging_tell_pyFd(PyObject *fd);
#include "ImagingUtils.h" #include "ImagingUtils.h"
extern UINT8 *clip8_lookups; extern UINT8 *clip8_lookups;
/* Mutex lock/unlock helpers */
#ifdef Py_GIL_DISABLED
#define MUTEX_LOCK(m) PyMutex_Lock(m)
#define MUTEX_UNLOCK(m) PyMutex_Unlock(m)
#else
#define MUTEX_LOCK(m)
#define MUTEX_UNLOCK(m)
#endif
#if defined(__cplusplus) #if defined(__cplusplus)
} }
#endif #endif

View File

@ -258,16 +258,6 @@ void
ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) { ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) {
int i = 0; int i = 0;
/* RGB triplets */ /* RGB triplets */
#ifdef __sparc
/* SPARC CPUs cannot read integers from nonaligned addresses. */
for (; i < pixels; i++) {
out[0] = in[R];
out[1] = in[G];
out[2] = in[B];
out += 3;
in += 4;
}
#else
for (; i < pixels - 1; i++) { for (; i < pixels - 1; i++) {
memcpy(out, in + i * 4, 4); memcpy(out, in + i * 4, 4);
out += 3; out += 3;
@ -278,7 +268,6 @@ ImagingPackRGB(UINT8 *out, const UINT8 *in, int pixels) {
out[2] = in[i * 4 + B]; out[2] = in[i * 4 + B];
out += 3; out += 3;
} }
#endif
} }
void void

View File

@ -36,7 +36,8 @@
#define UINT32_MAX 0xffffffff #define UINT32_MAX 0xffffffff
#endif #endif
#define NO_OUTPUT // #define DEBUG
// #define TEST_NEAREST_NEIGHBOUR
typedef struct { typedef struct {
uint32_t scale; uint32_t scale;
@ -144,7 +145,7 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) {
PixelHashData *d; PixelHashData *d;
HashTable *hash; HashTable *hash;
uint32_t i; uint32_t i;
#ifndef NO_OUTPUT #ifdef DEBUG
uint32_t timer, timer2, timer3; uint32_t timer, timer2, timer3;
#endif #endif
@ -156,7 +157,7 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) {
hash = hashtable_new(pixel_hash, pixel_cmp); hash = hashtable_new(pixel_hash, pixel_cmp);
hashtable_set_user_data(hash, d); hashtable_set_user_data(hash, d);
d->scale = 0; d->scale = 0;
#ifndef NO_OUTPUT #ifdef DEBUG
timer = timer3 = clock(); timer = timer3 = clock();
#endif #endif
for (i = 0; i < nPixels; i++) { for (i = 0; i < nPixels; i++) {
@ -167,22 +168,22 @@ create_pixel_hash(Pixel *pixelData, uint32_t nPixels) {
} }
while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) { while (hashtable_get_count(hash) > MAX_HASH_ENTRIES) {
d->scale++; d->scale++;
#ifndef NO_OUTPUT #ifdef DEBUG
printf("rehashing - new scale: %d\n", (int)d->scale); printf("rehashing - new scale: %d\n", (int)d->scale);
timer2 = clock(); timer2 = clock();
#endif #endif
hashtable_rehash_compute(hash, rehash_collide); hashtable_rehash_compute(hash, rehash_collide);
#ifndef NO_OUTPUT #ifdef DEBUG
timer2 = clock() - timer2; timer2 = clock() - timer2;
printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC); printf("rehash took %f sec\n", timer2 / (double)CLOCKS_PER_SEC);
timer += timer2; timer += timer2;
#endif #endif
} }
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("inserts took %f sec\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
#ifndef NO_OUTPUT #ifdef DEBUG
printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC); printf("total %f sec\n", (clock() - timer3) / (double)CLOCKS_PER_SEC);
#endif #endif
return hash; return hash;
@ -304,18 +305,18 @@ mergesort_pixels(PixelList *head, int i) {
return head; return head;
} }
#if defined(TEST_MERGESORT) || defined(TEST_SORTED) #ifdef DEBUG
static int static int
test_sorted(PixelList *pl[3]) { test_sorted(PixelList *pl[3]) {
int i, n, l; int i, l;
PixelList *t; PixelList *t;
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
n = 0;
l = 256; l = 256;
for (t = pl[i]; t; t = t->next[i]) { for (t = pl[i]; t; t = t->next[i]) {
if (l < t->p.a.v[i]) if (l < t->p.a.v[i]) {
return 0; return 0;
}
l = t->p.a.v[i]; l = t->p.a.v[i];
} }
} }
@ -347,12 +348,12 @@ splitlists(
PixelList *l, *r, *c, *n; PixelList *l, *r, *c, *n;
int i; int i;
int nRight; int nRight;
#ifndef NO_OUTPUT #ifdef DEBUG
int nLeft; int nLeft;
#endif #endif
int splitColourVal; int splitColourVal;
#ifdef TEST_SPLIT #ifdef DEBUG
{ {
PixelList *_prevTest, *_nextTest; PixelList *_prevTest, *_nextTest;
int _i, _nextCount[3], _prevCount[3]; int _i, _nextCount[3], _prevCount[3];
@ -402,14 +403,14 @@ splitlists(
#endif #endif
nCount[0] = nCount[1] = 0; nCount[0] = nCount[1] = 0;
nRight = 0; nRight = 0;
#ifndef NO_OUTPUT #ifdef DEBUG
nLeft = 0; nLeft = 0;
#endif #endif
for (left = 0, c = h[axis]; c;) { for (left = 0, c = h[axis]; c;) {
left = left + c->count; left = left + c->count;
nCount[0] += c->count; nCount[0] += c->count;
c->flag = 0; c->flag = 0;
#ifndef NO_OUTPUT #ifdef DEBUG
nLeft++; nLeft++;
#endif #endif
c = c->next[axis]; c = c->next[axis];
@ -424,7 +425,7 @@ splitlists(
break; break;
} }
c->flag = 0; c->flag = 0;
#ifndef NO_OUTPUT #ifdef DEBUG
nLeft++; nLeft++;
#endif #endif
nCount[0] += c->count; nCount[0] += c->count;
@ -442,14 +443,14 @@ splitlists(
} }
c->flag = 1; c->flag = 1;
nRight++; nRight++;
#ifndef NO_OUTPUT #ifdef DEBUG
nLeft--; nLeft--;
#endif #endif
nCount[0] -= c->count; nCount[0] -= c->count;
nCount[1] += c->count; nCount[1] += c->count;
} }
} }
#ifndef NO_OUTPUT #ifdef DEBUG
if (!nLeft) { if (!nLeft) {
for (c = h[axis]; c; c = c->next[axis]) { for (c = h[axis]; c; c = c->next[axis]) {
printf("[%d %d %d]\n", c->p.c.r, c->p.c.g, c->p.c.b); printf("[%d %d %d]\n", c->p.c.r, c->p.c.g, c->p.c.b);
@ -511,7 +512,7 @@ split(BoxNode *node) {
gl = node->tail[1]->p.c.g; gl = node->tail[1]->p.c.g;
bh = node->head[2]->p.c.b; bh = node->head[2]->p.c.b;
bl = node->tail[2]->p.c.b; bl = node->tail[2]->p.c.b;
#ifdef TEST_SPLIT #ifdef DEBUG
printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh); printf("splitting node [%d %d %d] [%d %d %d] ", rl, gl, bl, rh, gh, bh);
#endif #endif
f[0] = (rh - rl) * 77; f[0] = (rh - rl) * 77;
@ -526,11 +527,8 @@ split(BoxNode *node) {
axis = i; axis = i;
} }
} }
#ifdef TEST_SPLIT #ifdef DEBUG
printf("along axis %d\n", axis + 1); printf("along axis %d\n", axis + 1);
#endif
#ifdef TEST_SPLIT
{ {
PixelList *_prevTest, *_nextTest; PixelList *_prevTest, *_nextTest;
int _i, _nextCount[3], _prevCount[3]; int _i, _nextCount[3], _prevCount[3];
@ -592,12 +590,12 @@ split(BoxNode *node) {
if (!splitlists( if (!splitlists(
node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount node->head, node->tail, heads, tails, newCounts, axis, node->pixelCount
)) { )) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf("list split failed.\n"); printf("list split failed.\n");
#endif #endif
return 0; return 0;
} }
#ifdef TEST_SPLIT #ifdef DEBUG
if (!test_sorted(heads[0])) { if (!test_sorted(heads[0])) {
printf("bug in split"); printf("bug in split");
exit(1); exit(1);
@ -623,7 +621,7 @@ split(BoxNode *node) {
node->head[i] = NULL; node->head[i] = NULL;
node->tail[i] = NULL; node->tail[i] = NULL;
} }
#ifdef TEST_SPLIT #ifdef DEBUG
if (left->head[0]) { if (left->head[0]) {
rh = left->head[0]->p.c.r; rh = left->head[0]->p.c.r;
rl = left->tail[0]->p.c.r; rl = left->tail[0]->p.c.r;
@ -687,7 +685,7 @@ median_cut(PixelList *hl[3], uint32_t imPixelCount, int nPixels) {
} }
} while (compute_box_volume(thisNode) == 1); } while (compute_box_volume(thisNode) == 1);
if (!split(thisNode)) { if (!split(thisNode)) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf("Oops, split failed...\n"); printf("Oops, split failed...\n");
#endif #endif
exit(1); exit(1);
@ -716,16 +714,14 @@ free_box_tree(BoxNode *n) {
free(n); free(n);
} }
#ifdef TEST_SPLIT_INTEGRITY #ifdef DEBUG
static int static int
checkContained(BoxNode *n, Pixel *pp) { checkContained(BoxNode *n, Pixel *pp) {
if (n->l && n->r) { if (n->l && n->r) {
return checkContained(n->l, pp) + checkContained(n->r, pp); return checkContained(n->l, pp) + checkContained(n->r, pp);
} }
if (n->l || n->r) { if (n->l || n->r) {
#ifndef NO_OUTPUT
printf("box tree is dead\n"); printf("box tree is dead\n");
#endif
return 0; return 0;
} }
if (pp->c.r <= n->head[0]->p.c.r && pp->c.r >= n->tail[0]->p.c.r && if (pp->c.r <= n->head[0]->p.c.r && pp->c.r >= n->tail[0]->p.c.r &&
@ -746,7 +742,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) {
return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box); return annotate_hash_table(n->l, h, box) && annotate_hash_table(n->r, h, box);
} }
if (n->l || n->r) { if (n->l || n->r) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf("box tree is dead\n"); printf("box tree is dead\n");
#endif #endif
return 0; return 0;
@ -754,7 +750,7 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) {
for (p = n->head[0]; p; p = p->next[0]) { for (p = n->head[0]; p; p = p->next[0]) {
PIXEL_UNSCALE(&(p->p), &q, d->scale); PIXEL_UNSCALE(&(p->p), &q, d->scale);
if (!hashtable_insert(h, q, *box)) { if (!hashtable_insert(h, q, *box)) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf("hashtable insert failed\n"); printf("hashtable insert failed\n");
#endif #endif
return 0; return 0;
@ -978,7 +974,7 @@ map_image_pixels_from_median_box(
continue; continue;
} }
if (!hashtable_lookup(medianBoxHash, pixelData[i], &pixelVal)) { if (!hashtable_lookup(medianBoxHash, pixelData[i], &pixelVal)) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf("pixel lookup failed\n"); printf("pixel lookup failed\n");
#endif #endif
return 0; return 0;
@ -1014,7 +1010,8 @@ compute_palette_from_median_cut(
uint32_t nPixels, uint32_t nPixels,
HashTable *medianBoxHash, HashTable *medianBoxHash,
Pixel **palette, Pixel **palette,
uint32_t nPaletteEntries uint32_t nPaletteEntries,
BoxNode *root
) { ) {
uint32_t i; uint32_t i;
uint32_t paletteEntry; uint32_t paletteEntry;
@ -1043,7 +1040,7 @@ compute_palette_from_median_cut(
} }
} }
for (i = 0; i < nPixels; i++) { for (i = 0; i < nPixels; i++) {
#ifdef TEST_SPLIT_INTEGRITY #ifdef DEBUG
if (!(i % 100)) { if (!(i % 100)) {
printf("%05d\r", i); printf("%05d\r", i);
fflush(stdout); fflush(stdout);
@ -1058,7 +1055,7 @@ compute_palette_from_median_cut(
} }
#endif #endif
if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) { if (!hashtable_lookup(medianBoxHash, pixelData[i], &paletteEntry)) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf("pixel lookup failed\n"); printf("pixel lookup failed\n");
#endif #endif
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
@ -1068,7 +1065,7 @@ compute_palette_from_median_cut(
return 0; return 0;
} }
if (paletteEntry >= nPaletteEntries) { if (paletteEntry >= nPaletteEntries) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf( printf(
"panic - paletteEntry>=nPaletteEntries (%d>=%d)\n", "panic - paletteEntry>=nPaletteEntries (%d>=%d)\n",
(int)paletteEntry, (int)paletteEntry,
@ -1140,7 +1137,7 @@ compute_palette_from_quantized_pixels(
} }
for (i = 0; i < nPixels; i++) { for (i = 0; i < nPixels; i++) {
if (qp[i] >= nPaletteEntries) { if (qp[i] >= nPaletteEntries) {
#ifndef NO_OUTPUT #ifdef DEBUG
printf("scream\n"); printf("scream\n");
#endif #endif
return 0; return 0;
@ -1208,7 +1205,7 @@ k_means(
goto error_2; goto error_2;
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf("["); printf("[");
fflush(stdout); fflush(stdout);
#endif #endif
@ -1243,7 +1240,7 @@ k_means(
if (changes < 0) { if (changes < 0) {
goto error_3; goto error_3;
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf(".(%d)", changes); printf(".(%d)", changes);
fflush(stdout); fflush(stdout);
#endif #endif
@ -1251,7 +1248,7 @@ k_means(
break; break;
} }
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf("]\n"); printf("]\n");
#endif #endif
if (avgDistSortKey) { if (avgDistSortKey) {
@ -1311,32 +1308,32 @@ quantize(
uint32_t **avgDistSortKey; uint32_t **avgDistSortKey;
Pixel *p; Pixel *p;
#ifndef NO_OUTPUT #ifdef DEBUG
uint32_t timer, timer2; uint32_t timer, timer2;
#endif #endif
#ifndef NO_OUTPUT #ifdef DEBUG
timer2 = clock(); timer2 = clock();
printf("create hash table..."); printf("create hash table...");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
#endif #endif
h = create_pixel_hash(pixelData, nPixels); h = create_pixel_hash(pixelData, nPixels);
#ifndef NO_OUTPUT #ifdef DEBUG
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
if (!h) { if (!h) {
goto error_0; goto error_0;
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf("create lists from hash table..."); printf("create lists from hash table...");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
#endif #endif
hl[0] = hl[1] = hl[2] = NULL; hl[0] = hl[1] = hl[2] = NULL;
hashtable_foreach(h, hash_to_list, hl); hashtable_foreach(h, hash_to_list, hl);
#ifndef NO_OUTPUT #ifdef DEBUG
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
@ -1344,7 +1341,7 @@ quantize(
goto error_1; goto error_1;
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf("mergesort lists..."); printf("mergesort lists...");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
@ -1352,47 +1349,47 @@ quantize(
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
hl[i] = mergesort_pixels(hl[i], i); hl[i] = mergesort_pixels(hl[i], i);
} }
#ifdef TEST_MERGESORT #ifdef DEBUG
if (!test_sorted(hl)) { if (!test_sorted(hl)) {
printf("bug in mergesort\n"); printf("bug in mergesort\n");
goto error_1; goto error_1;
} }
#endif
#ifndef NO_OUTPUT
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
#ifndef NO_OUTPUT #ifdef DEBUG
printf("median cut..."); printf("median cut...");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
#endif #endif
root = median_cut(hl, nPixels, nQuantPixels); root = median_cut(hl, nPixels, nQuantPixels);
#ifndef NO_OUTPUT #ifdef DEBUG
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
if (!root) { if (!root) {
goto error_1; goto error_1;
} }
nPaletteEntries = 0; nPaletteEntries = 0;
#ifndef NO_OUTPUT #ifdef DEBUG
printf("median cut tree to hash table..."); printf("median cut tree to hash table...");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
#endif #endif
annotate_hash_table(root, h, &nPaletteEntries); annotate_hash_table(root, h, &nPaletteEntries);
#ifndef NO_OUTPUT #ifdef DEBUG
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
#ifndef NO_OUTPUT #ifdef DEBUG
printf("compute palette...\n"); printf("compute palette...\n");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
#endif #endif
if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { if (!compute_palette_from_median_cut(
pixelData, nPixels, h, &p, nPaletteEntries, root
)) {
goto error_3; goto error_3;
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
@ -1479,7 +1476,7 @@ quantize(
hashtable_free(h2); hashtable_free(h2);
} }
#endif #endif
#ifndef NO_OUTPUT #ifdef DEBUG
printf("k means...\n"); printf("k means...\n");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
@ -1487,7 +1484,7 @@ quantize(
if (kmeans > 0) { if (kmeans > 0) {
k_means(pixelData, nPixels, p, nPaletteEntries, qp, kmeans - 1); k_means(pixelData, nPixels, p, nPaletteEntries, qp, kmeans - 1);
} }
#ifndef NO_OUTPUT #ifdef DEBUG
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
#endif #endif
@ -1495,7 +1492,7 @@ quantize(
*palette = p; *palette = p;
*paletteLength = nPaletteEntries; *paletteLength = nPaletteEntries;
#ifndef NO_OUTPUT #ifdef DEBUG
printf("cleanup..."); printf("cleanup...");
fflush(stdout); fflush(stdout);
timer = clock(); timer = clock();
@ -1507,7 +1504,7 @@ quantize(
free(avgDistSortKey); free(avgDistSortKey);
} }
destroy_pixel_hash(h); destroy_pixel_hash(h);
#ifndef NO_OUTPUT #ifdef DEBUG
printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC); printf("done (%f)\n", (clock() - timer) / (double)CLOCKS_PER_SEC);
printf("-----\ntotal time %f\n", (clock() - timer2) / (double)CLOCKS_PER_SEC); printf("-----\ntotal time %f\n", (clock() - timer2) / (double)CLOCKS_PER_SEC);
#endif #endif

View File

@ -218,7 +218,9 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
break; break;
} }
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingDefaultArena.stats_new_count += 1; ImagingDefaultArena.stats_new_count += 1;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
return im; return im;
} }
@ -267,7 +269,10 @@ struct ImagingMemoryArena ImagingDefaultArena = {
0, 0,
0, 0,
0, 0,
0 // Stats 0, // Stats
#ifdef Py_GIL_DISABLED
{0},
#endif
}; };
int int
@ -364,18 +369,19 @@ ImagingDestroyArray(Imaging im) {
int y = 0; int y = 0;
if (im->blocks) { if (im->blocks) {
MUTEX_LOCK(&ImagingDefaultArena.mutex);
while (im->blocks[y].ptr) { while (im->blocks[y].ptr) {
memory_return_block(&ImagingDefaultArena, im->blocks[y]); memory_return_block(&ImagingDefaultArena, im->blocks[y]);
y += 1; y += 1;
} }
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
free(im->blocks); free(im->blocks);
} }
} }
Imaging Imaging
ImagingAllocateArray(Imaging im, int dirty, int block_size) { ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_size) {
int y, line_in_block, current_block; int y, line_in_block, current_block;
ImagingMemoryArena arena = &ImagingDefaultArena;
ImagingMemoryBlock block = {NULL, 0}; ImagingMemoryBlock block = {NULL, 0};
int aligned_linesize, lines_per_block, blocks_count; int aligned_linesize, lines_per_block, blocks_count;
char *aligned_ptr = NULL; char *aligned_ptr = NULL;
@ -498,14 +504,22 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) {
return NULL; return NULL;
} }
if (ImagingAllocateArray(im, dirty, ImagingDefaultArena.block_size)) { MUTEX_LOCK(&ImagingDefaultArena.mutex);
Imaging tmp = ImagingAllocateArray(
im, &ImagingDefaultArena, dirty, ImagingDefaultArena.block_size
);
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
if (tmp) {
return im; return im;
} }
ImagingError_Clear(); ImagingError_Clear();
// Try to allocate the image once more with smallest possible block size // Try to allocate the image once more with smallest possible block size
if (ImagingAllocateArray(im, dirty, IMAGING_PAGE_SIZE)) { MUTEX_LOCK(&ImagingDefaultArena.mutex);
tmp = ImagingAllocateArray(im, &ImagingDefaultArena, dirty, IMAGING_PAGE_SIZE);
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
if (tmp) {
return im; return im;
} }

11
tox.ini
View File

@ -33,16 +33,7 @@ commands =
skip_install = true skip_install = true
deps = deps =
-r .ci/requirements-mypy.txt -r .ci/requirements-mypy.txt
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pytest
types-defusedxml
types-olefile
types-setuptools
extras = extras =
typing typing
commands = commands =
mypy src Tests {posargs} mypy docs src winbuild Tests {posargs}

View File

@ -7,6 +7,7 @@ import re
import shutil import shutil
import struct import struct
import subprocess import subprocess
from typing import Any
def cmd_cd(path: str) -> str: def cmd_cd(path: str) -> str:
@ -43,21 +44,19 @@ def cmd_nmake(
target: str = "", target: str = "",
params: list[str] | None = None, params: list[str] | None = None,
) -> str: ) -> str:
params = "" if params is None else " ".join(params)
return " ".join( return " ".join(
[ [
"{nmake}", "{nmake}",
"-nologo", "-nologo",
f'-f "{makefile}"' if makefile is not None else "", f'-f "{makefile}"' if makefile is not None else "",
f"{params}", f'{" ".join(params)}' if params is not None else "",
f'"{target}"', f'"{target}"',
] ]
) )
def cmds_cmake( def cmds_cmake(
target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "."
) -> list[str]: ) -> list[str]:
if not isinstance(target, str): if not isinstance(target, str):
target = " ".join(target) target = " ".join(target)
@ -129,7 +128,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "")
# dependencies, listed in order of compilation # dependencies, listed in order of compilation
DEPS = { DEPS: dict[str, dict[str, Any]] = {
"libjpeg": { "libjpeg": {
"url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/"
f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download",
@ -201,7 +200,7 @@ DEPS = {
}, },
"build": [ "build": [
*cmds_cmake( *cmds_cmake(
"webp webpdemux webpmux", "webp webpmux webpdemux",
"-DBUILD_SHARED_LIBS:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF",
"-DWEBP_LINK_STATIC:BOOL=OFF", "-DWEBP_LINK_STATIC:BOOL=OFF",
), ),
@ -538,7 +537,7 @@ def write_script(
print(" " + line) print(" " + line)
def get_footer(dep: dict) -> list[str]: def get_footer(dep: dict[str, Any]) -> list[str]:
lines = [] lines = []
for out in dep.get("headers", []): for out in dep.get("headers", []):
lines.append(cmd_copy(out, "{inc_dir}")) lines.append(cmd_copy(out, "{inc_dir}"))
@ -583,6 +582,7 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str:
license_text += f.read() license_text += f.read()
if "license_pattern" in dep: if "license_pattern" in dep:
match = re.search(dep["license_pattern"], license_text, re.DOTALL) match = re.search(dep["license_pattern"], license_text, re.DOTALL)
assert match is not None
license_text = "\n".join(match.groups()) license_text = "\n".join(match.groups())
assert len(license_text) > 50 assert len(license_text) > 50
with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: