Merge branch 'main' into type_hint_init

This commit is contained in:
Andrew Murray 2024-08-28 08:31:09 +10:00 committed by GitHub
commit eae107ceb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 589 additions and 652 deletions

View File

@ -51,7 +51,7 @@ build_script:
test_script:
- 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%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%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 coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov

View File

@ -5,6 +5,7 @@ ipython
numpy
packaging
pytest
sphinx
types-defusedxml
types-olefile
types-setuptools

View File

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

View File

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

View File

@ -87,7 +87,7 @@ jobs:
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images

View File

@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.6
rev: v0.6.0
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
rev: 24.8.0
hooks:
- id: black
@ -67,7 +67,7 @@ repos:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.18
rev: v0.19
hooks:
- id: validate-pyproject

View File

@ -5,6 +5,21 @@ Changelog (Pillow)
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]

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

View File

@ -10,11 +10,6 @@ from PIL import features
from .helper import skip_unless_feature
try:
from PIL import _webp
except ImportError:
pass
def test_check() -> None:
# Check the correctness of the convenience function
@ -23,7 +18,11 @@ def test_check() -> None:
for codec in features.codecs:
assert features.check_codec(codec) == features.check(codec)
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:
@ -48,23 +47,26 @@ def test_version() -> None:
for codec in features.codecs:
test(codec, features.version_codec)
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:
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha()
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY
with pytest.warns(DeprecationWarning):
assert features.check("transp_webp") == features.check_module("webp")
@skip_unless_feature("webp")
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:
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")

View File

@ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
# 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:
assert im.info["background"] == (255, 255, 255, 255)
im.save(out)

View File

@ -77,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None:
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:
# Arrange
with Image.open("Tests/images/hopper.tif") as im:

View File

@ -1019,13 +1019,16 @@ class TestFileJpeg:
# SOI, EOI
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
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)
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[1] + data[2])) as combined_im:

View File

@ -233,7 +233,7 @@ def test_layers() -> None:
("foo.jp2", {"no_jp2": True}, 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"),
(None, {"no_jp2": False}, 4, b"jP"),
),
)
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:
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"))
def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")

View File

@ -48,8 +48,6 @@ class TestFileWebp:
self.rgb_mode = "RGB"
def test_version(self) -> None:
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
version = features.version_module("webp")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
@ -117,7 +115,6 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
@skip_unless_feature("webp_anim")
def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (1, 1))
@ -132,10 +129,9 @@ class TestFileWebp:
def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
if _webp.HAVE_WEBPANIM:
self._roundtrip(
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
)
self._roundtrip(
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
)
def test_write_unsupported_mode_L(self, tmp_path: Path) -> None:
"""
@ -161,27 +157,32 @@ class TestFileWebp:
im.save(temp_file, method=0)
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:
"""
Calling encoder functions with no arguments should result in an error.
"""
if _webp.HAVE_WEBPANIM:
with pytest.raises(TypeError):
_webp.WebPAnimEncoder()
with pytest.raises(TypeError):
_webp.WebPAnimEncoder()
with pytest.raises(TypeError):
_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.
"""
if _webp.HAVE_WEBPANIM:
with pytest.raises(TypeError):
_webp.WebPAnimDecoder()
with pytest.raises(TypeError):
_webp.WebPDecode()
_webp.WebPAnimDecoder()
def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp"
@ -200,7 +201,6 @@ class TestFileWebp:
"background",
(0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
)
@skip_unless_feature("webp_anim")
def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
@ -209,7 +209,6 @@ class TestFileWebp:
with pytest.raises(OSError):
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:
# Save L mode GIF with background
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))
assert difference < 5
@skip_unless_feature("webp_anim")
def test_duration(self, tmp_path: Path) -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000

View File

@ -13,12 +13,7 @@ from .helper import (
hopper,
)
_webp = 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")
pytest.importorskip("PIL._webp", reason="WebP support not installed")
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.save(temp_file)
if _webp.WebPDecoderBuggyAlpha():
return
with Image.open(temp_file) as image:
image.load()
@ -93,12 +85,7 @@ def test_write_rgba(tmp_path: Path) -> None:
image.load()
image.getdata()
# Early versions of WebP are known to produce higher deviations:
# deal with it
if _webp.WebPDecoderVersion() <= 0x201:
assert_image_similar(image, pil_image, 3.0)
else:
assert_image_similar(image, pil_image, 1.0)
assert_image_similar(image, pil_image, 1.0)
def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:

View File

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

View File

@ -8,14 +8,11 @@ from PIL import Image
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"
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")
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
pytestmark = [
skip_unless_feature("webp"),
skip_unless_feature("webp_mux"),
]
pytestmark = skip_unless_feature("webp")
ElementTree: ModuleType | None
try:
@ -119,7 +116,15 @@ def test_read_no_exif() -> None:
def test_getxmp() -> None:
with Image.open("Tests/images/flower.webp") as im:
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:
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:
iccp_data = b"<iccp_data>"
exif_data = b"<exif_data>"

View File

@ -42,6 +42,12 @@ try:
except ImportError:
ElementTree = None
PrettyPrinter: type | None
try:
from IPython.lib.pretty import PrettyPrinter
except ImportError:
PrettyPrinter = None
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -91,16 +97,15 @@ class TestImage:
# with pytest.raises(MemoryError):
# Image.new("L", (1000000, 1000000))
@pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
def test_repr_pretty(self) -> None:
class Pretty:
def text(self, text: str) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100))
p = Pretty()
output = io.StringIO()
assert PrettyPrinter is not None
p = PrettyPrinter(output)
im._repr_pretty_(p, False)
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>"
assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"
def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png"
@ -818,7 +823,6 @@ class TestImage:
assert reloaded_exif[305] == "Pillow test"
@skip_unless_feature("webp")
@skip_unless_feature("webp_anim")
def test_exif_webp(self, tmp_path: Path) -> None:
with Image.open("Tests/images/hopper.webp") as im:
exif = im.getexif()
@ -940,7 +944,15 @@ class TestImage:
def test_empty_xmp(self) -> None:
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:
im = Image.new("RGB", (1, 1))

View File

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

View File

@ -398,7 +398,8 @@ def test_logical() -> None:
for y in (a, b):
imy = Image.new("1", (1, 1), y)
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)
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(
"xy, radius, type",
[

View File

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

View File

@ -46,7 +46,8 @@ def img_to_string(im: Image.Image) -> str:
line = ""
for c in range(im.width):
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]
result.append(line)
return "\n".join(result)

View File

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

View File

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

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Union
import pytest
@ -8,6 +9,20 @@ from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
if TYPE_CHECKING:
import PyQt6
import PySide6
QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@ -20,7 +35,7 @@ if ImageQt.qt_is_installed:
from PySide6.QtGui import QImage, QPainter, QRegion
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
class Example(QWidget):
class Example(QWidget): # type: ignore[misc]
def __init__(self) -> None:
super().__init__()
@ -28,11 +43,12 @@ if ImageQt.qt_is_installed:
qimage = ImageQt.ImageQt(img)
pixmap1 = ImageQt.QPixmap.fromImage(qimage)
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
QHBoxLayout(self) # hbox
# hbox
QHBoxLayout(self) # type: ignore[operator]
lbl = QLabel(self)
lbl = QLabel(self) # type: ignore[operator]
# Segfault in the problem
lbl.setPixmap(pixmap1.copy())
@ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None:
# Segfault test
app: QApplication | None = QApplication([])
app: QApplication | None = QApplication([]) # type: ignore[operator]
ex = Example()
assert app # Silence warning
assert ex # Silence warning
@ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None:
im = hopper(mode)
data = ImageQt.toqpixmap(im)
assert isinstance(data, QPixmap)
assert data.__class__.__name__ == "QPixmap"
assert not data.isNull()
# Test saving the file
@ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None:
data.save(tempfile)
# Render the image
qimage = ImageQt.ImageQt(im)
data = QPixmap.fromImage(qimage)
qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, qt_format.Format_ARGB32)
painter = QPainter(qimage)
image_label = QLabel()
imageqt = ImageQt.ImageQt(im)
data = getattr(QPixmap, "fromImage")(imageqt)
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
painter = QPainter(qimage) # type: ignore[operator]
image_label = QLabel() # type: ignore[operator]
image_label.setPixmap(data)
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
painter.end()
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
qimage.save(rendered_tempfile)

View File

@ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
assert isinstance(data, QImage)
assert isinstance(data, QImage) # type: ignore[arg-type, misc]
assert not data.isNull()
# reload directly from the qimage

View File

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

View File

@ -126,6 +126,16 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
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
----------------

View File

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

View File

@ -1220,8 +1220,7 @@ using the general tags available through tiffinfo.
WebP
^^^^
Pillow reads and writes WebP files. The specifics of Pillow's capabilities with
this format are currently undocumented.
Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later.
.. _webp-saving:
@ -1249,29 +1248,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**exact**
If true, preserve the transparent RGB values. Otherwise, discard
invisible RGB values for better compression. Defaults to false.
Requires libwebp 0.5.0 or later.
**icc_profile**
The ICC Profile to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
The ICC Profile to include in the saved file.
**exif**
The exif data to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
The exif data to include in the saved file.
**xmp**
The XMP data to include in the saved file. Only supported if
the system WebP library was built with webpmux support.
The XMP data to include in the saved file.
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
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
@ -1528,19 +1517,21 @@ To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler. ::
from PIL import Image
from typing import IO
from PIL import Image, ImageFile
from PIL import WmfImagePlugin
class WmfHandler:
def open(self, im):
class WmfHandler(ImageFile.StubHandler):
def open(self, im: ImageFile.StubImageFile) -> None:
...
def load(self, im):
def load(self, im: ImageFile.StubImageFile) -> Image.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."""
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]
h = max(im1.size[1], im2.size[1])
im = Image.new("RGBA", (w, h))
@ -704,7 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality.
import glob
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:
if img.mode != "RGB":
img = img.convert("RGB")

View File

@ -53,7 +53,7 @@ true color.
from PIL import Image, ImageFile
def _accept(prefix):
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM"
@ -62,7 +62,7 @@ true color.
format = "SPAM"
format_description = "Spam raster image"
def _open(self):
def _open(self) -> None:
header = self.fp.read(128).split()
@ -82,7 +82,7 @@ true color.
raise SyntaxError(msg)
# 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)

View File

@ -55,10 +55,6 @@ Many of Pillow's features require external libraries:
* **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.
* 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``,
``-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``.
Disable building the corresponding feature even if the development
libraries are present on the building machine.
* Config settings: ``-C zlib=enable``, ``-C jpeg=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``.
Require that the corresponding feature is built. The build will raise
an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together.
an exception if the libraries are not found. Tcl and Tk must be used
together.
* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``.
These flags are used to compile a modified version of libraqm and

View File

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

View File

@ -54,12 +54,12 @@ Feature version numbers are available only where stated.
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.
* ``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.
* ``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.
* ``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.version_feature

View File

@ -58,6 +58,14 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc
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
===========

View File

@ -109,7 +109,7 @@ lint.select = [
"ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks
"PT006", # pytest-parametrize-names-wrong-type
"PT", # flake8-pytest-style
"PYI", # flake8-pyi
"RUF100", # unused noqa (yesqa)
"UP", # pyupgrade
@ -121,6 +121,12 @@ lint.ignore = [
"E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator
"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
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
]
@ -160,5 +166,4 @@ warn_unused_ignores = true
exclude = [
'^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$',
'^Tests/test_qt_image_qapplication.py$',
]

View File

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

View File

@ -478,7 +478,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(struct.pack("<i", 5))
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)

View File

@ -484,7 +484,9 @@ def _save(
if 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

@ -433,7 +433,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -
if hasattr(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"grestore end\n")

View File

@ -601,7 +601,9 @@ def _write_single_frame(
_write_local_header(fp, im, (0, 0), flags)
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
@ -1069,7 +1071,9 @@ def _write_frame_data(
_write_local_header(fp, im_frame, offset, 0)
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

View File

@ -97,7 +97,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if bits != 32:
and_mask = Image.new("1", size)
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:
frame.save(image_io, "png")
@ -317,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile):
self.load()
@property
def size(self):
def size(self) -> tuple[int, int]:
return self._size
@size.setter
def size(self, value):
def size(self, value: tuple[int, int]) -> None:
if value not in self.info["sizes"]:
msg = "This is not one of the allowed sizes of this image"
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 += b"\x00" * (256 - colors)
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
if TYPE_CHECKING:
import mmap
from xml.etree.ElementTree import Element
from . import ImageFile, ImagePalette, TiffImagePlugin
from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
OPEN: dict[
@ -467,43 +470,53 @@ def _getencoder(
# 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:
self.scale = scale
self.offset = offset
def __neg__(self) -> _E:
return _E(-self.scale, -self.offset)
def __neg__(self) -> ImagePointTransform:
return ImagePointTransform(-self.scale, -self.offset)
def __add__(self, other: _E | float) -> _E:
if isinstance(other, _E):
return _E(self.scale + other.scale, self.offset + other.offset)
return _E(self.scale, self.offset + other)
def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return ImagePointTransform(
self.scale + other.scale, self.offset + other.offset
)
return ImagePointTransform(self.scale, self.offset + other)
__radd__ = __add__
def __sub__(self, other: _E | float) -> _E:
def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return self + -other
def __rsub__(self, other: _E | float) -> _E:
def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return other + -self
def __mul__(self, other: _E | float) -> _E:
if isinstance(other, _E):
def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return NotImplemented
return _E(self.scale * other, self.offset * other)
return ImagePointTransform(self.scale * other, self.offset * other)
__rmul__ = __mul__
def __truediv__(self, other: _E | float) -> _E:
if isinstance(other, _E):
def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return NotImplemented
return _E(self.scale / other, self.offset / other)
return ImagePointTransform(self.scale / other, self.offset / other)
def _getscaleoffset(expr) -> tuple[float, float]:
a = expr(_E(1, 0))
return (a.scale, a.offset) if isinstance(a, _E) else (0, a)
def _getscaleoffset(
expr: Callable[[ImagePointTransform], ImagePointTransform | float]
) -> tuple[float, float]:
a = expr(ImagePointTransform(1, 0))
return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)
# --------------------------------------------------------------------
@ -623,7 +636,7 @@ class Image:
logger.debug("Error closing: %s", msg)
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
# deferred error that will better explain that the core image
@ -687,7 +700,7 @@ class Image:
id(self),
)
def _repr_pretty_(self, p, cycle: bool) -> None:
def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
"""IPython plain text display support"""
# Same as __repr__ but without unpredictable id(self),
@ -734,24 +747,12 @@ class Image:
def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]:
# numpy array interface support
new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3}
try:
if self.mode == "1":
# Binary images need to be extended from bits to bytes
# See: https://github.com/python-pillow/Pillow/issues/350
new["data"] = self.tobytes("raw", "L")
else:
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
if self.mode == "1":
# Binary images need to be extended from bits to bytes
# See: https://github.com/python-pillow/Pillow/issues/350
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
new["shape"], new["typestr"] = _conv_type_shape(self)
return new
@ -1352,9 +1353,6 @@ class Image:
self.load()
return self._new(self.im.expand(xmargin, ymargin))
if TYPE_CHECKING:
from . import ImageFilter
def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
"""
Filters this image using the given filter. For a list of
@ -1918,7 +1916,13 @@ class Image:
def point(
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,
) -> Image:
"""
@ -1935,7 +1939,7 @@ class Image:
object::
class Example(Image.ImagePointHandler):
def point(self, data):
def point(self, im: Image) -> Image:
# Return result
: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
@ -1954,10 +1958,10 @@ class Image:
# check if the function can be used with point_transform
# UNDONE wiredfool -- I think this prevents us from ever doing
# 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))
# 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:
flatLut = lut
@ -2894,11 +2898,11 @@ class Image:
self,
box: tuple[int, int, int, int],
image: Image,
method,
data,
method: Transform,
data: Sequence[float],
resample: int = Resampling.NEAREST,
fill: bool = True,
):
) -> None:
w = box[2] - box[0]
h = box[3] - box[1]
@ -2999,7 +3003,7 @@ class Image:
self.load()
return self._new(self.im.effect_spread(distance))
def toqimage(self):
def toqimage(self) -> ImageQt.ImageQt:
"""Returns a QImage copy of this image"""
from . import ImageQt
@ -3008,7 +3012,7 @@ class Image:
raise ImportError(msg)
return ImageQt.toqimage(self)
def toqpixmap(self):
def toqpixmap(self) -> ImageQt.QPixmap:
"""Returns a QPixmap copy of this image"""
from . import ImageQt
@ -3335,7 +3339,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
def fromqimage(im) -> ImageFile.ImageFile:
def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image"""
from . import ImageQt
@ -3345,7 +3349,7 @@ def fromqimage(im) -> ImageFile.ImageFile:
return ImageQt.fromqimage(im)
def fromqpixmap(im) -> ImageFile.ImageFile:
def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile:
"""Creates an image instance from a QPixmap image"""
from . import ImageQt
@ -4033,15 +4037,19 @@ class Exif(_ExifBase):
ifd[tag] = value
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 == ExifTags.IFD.IFD1:
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]:
offset = self._hidden_data.get(tag, self.get(tag))
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]:
if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif)
@ -4098,7 +4106,9 @@ class Exif(_ExifBase):
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)
camerainfo = {"ModelID": self.fp.read(4)}
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
self.fp.read(4)
# Seconds since 2000
@ -4114,16 +4124,18 @@ class Exif(_ExifBase):
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)
)[0]
self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
else:
# 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, {})
if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = {

View File

@ -504,7 +504,7 @@ class ImageDraw:
if full_x:
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)
if not full_x and not full_y:
left = [x0, y0, x0 + r, y1]

View File

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

View File

@ -19,14 +19,23 @@ from __future__ import annotations
import sys
from io import BytesIO
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Any, Callable, Union
from . import Image
from ._util import is_path
if TYPE_CHECKING:
import PyQt6
import PySide6
from . import ImageFile
QBuffer: type
QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray]
QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
qt_version: str | None
qt_versions = [
["6", "PyQt6"],
@ -37,10 +46,6 @@ qt_versions = [
qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
for version, qt_module in qt_versions:
try:
QBuffer: type
QIODevice: type
QImage: type
QPixmap: type
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice
@ -65,19 +70,20 @@ def rgb(r: int, g: int, b: int, a: int = 255) -> int:
return qRgba(r, g, b, a) & 0xFFFFFFFF
def fromqimage(im):
def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile:
"""
:param im: QImage or PIL ImageQt object
"""
buffer = QBuffer()
qt_openmode: object
if qt_version == "6":
try:
qt_openmode = QIODevice.OpenModeFlag
qt_openmode = getattr(QIODevice, "OpenModeFlag")
except AttributeError:
qt_openmode = QIODevice.OpenMode
qt_openmode = getattr(QIODevice, "OpenMode")
else:
qt_openmode = QIODevice
buffer.open(qt_openmode.ReadWrite)
buffer.open(getattr(qt_openmode, "ReadWrite"))
# preserve alpha channel with png
# otherwise ppm is more friendly with Image.open
if im.hasAlphaChannel():
@ -93,7 +99,7 @@ def fromqimage(im):
return Image.open(b)
def fromqpixmap(im) -> ImageFile.ImageFile:
def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile:
return fromqimage(im)
@ -123,7 +129,7 @@ def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
return b"".join(new_data)
def _toqclass_helper(im):
def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
data = None
colortable = None
exclusive_fp = False
@ -135,30 +141,32 @@ def _toqclass_helper(im):
if is_path(im):
im = Image.open(im)
exclusive_fp = True
assert isinstance(im, Image.Image)
qt_format = QImage.Format if qt_version == "6" else QImage
qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage
if im.mode == "1":
format = qt_format.Format_Mono
format = getattr(qt_format, "Format_Mono")
elif im.mode == "L":
format = qt_format.Format_Indexed8
format = getattr(qt_format, "Format_Indexed8")
colortable = [rgb(i, i, i) for i in range(256)]
elif im.mode == "P":
format = qt_format.Format_Indexed8
format = getattr(qt_format, "Format_Indexed8")
palette = im.getpalette()
assert palette is not None
colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
elif im.mode == "RGB":
# Populate the 4th channel with 255
im = im.convert("RGBA")
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_RGB32
format = getattr(qt_format, "Format_RGB32")
elif im.mode == "RGBA":
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_ARGB32
format = getattr(qt_format, "Format_ARGB32")
elif im.mode == "I;16":
im = im.point(lambda i: i * 256)
format = qt_format.Format_Grayscale16
format = getattr(qt_format, "Format_Grayscale16")
else:
if exclusive_fp:
im.close()
@ -174,8 +182,8 @@ def _toqclass_helper(im):
if qt_is_installed:
class ImageQt(QImage):
def __init__(self, im) -> None:
class ImageQt(QImage): # type: ignore[misc]
def __init__(self, im: Image.Image | str | QByteArray) -> None:
"""
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
class.
@ -199,10 +207,10 @@ if qt_is_installed:
self.setColorTable(im_data["colortable"])
def toqimage(im) -> ImageQt:
def toqimage(im: Image.Image | str | QByteArray) -> ImageQt:
return ImageQt(im)
def toqpixmap(im):
def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap:
qimage = toqimage(im)
return QPixmap.fromImage(qimage)
return getattr(QPixmap, "fromImage")(qimage)

View File

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

View File

@ -419,7 +419,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
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

@ -826,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.
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:

View File

@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(o16(h))
# 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
import sys
from typing import TYPE_CHECKING
from typing import IO, TYPE_CHECKING
from . import EpsImagePlugin
@ -28,15 +28,12 @@ from . import EpsImagePlugin
class PSDraw:
"""
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:
try:
fp = sys.stdout.buffer
except AttributeError:
fp = sys.stdout
fp = sys.stdout.buffer
self.fp = fp
def begin_document(self, id: str | None = None) -> None:

View File

@ -214,7 +214,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
)
# 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"):
fp.flush()

View File

@ -198,7 +198,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
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":
# colour palette

View File

@ -138,7 +138,7 @@ def _write_image(
op = io.BytesIO()
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 decode_filter == "CCITTFaxDecode":
im.save(
op,

View File

@ -1229,7 +1229,7 @@ def _write_multiple_frames(
ImageFile._save(
im,
cast(IO[bytes], _idat(fp, chunk)),
[("zip", (0, 0) + im.size, 0, rawmode)],
[ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)],
)
seq_num = 0
@ -1266,14 +1266,14 @@ def _write_multiple_frames(
ImageFile._save(
im_frame,
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:
fdat_chunks = _fdat(fp, chunk, seq_num)
ImageFile._save(
im_frame,
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
return None
@ -1474,7 +1474,7 @@ def _save(
ImageFile._save(
single_im,
cast(IO[bytes], _idat(fp, chunk)),
[("zip", (0, 0) + single_im.size, 0, rawmode)],
[ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
)
if info:

View File

@ -353,7 +353,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
elif head == b"Pf":
fp.write(b"-1.0\n")
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)
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:

View File

@ -236,11 +236,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if rle:
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:
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

View File

@ -259,6 +259,7 @@ OPEN_INFO = {
(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"),
(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"),
(MM, 6, (1,), 1, (8,), ()): ("L", "L"),
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
@ -455,8 +456,11 @@ class IFDRational(Rational):
__int__ = _delegate("__int__")
def _register_loader(idx: int, size: int):
def decorator(func):
_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any]
def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]:
def decorator(func: _LoaderFunc) -> _LoaderFunc:
from .TiffTags import TYPES
if func.__name__.startswith("load_"):
@ -481,12 +485,13 @@ def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None:
idx, fmt, name = idx_fmt_name
TYPES[idx] = name
size = struct.calcsize(f"={fmt}")
_load_dispatch[idx] = ( # noqa: F821
size,
lambda self, data, legacy_api=True: (
self._unpack(f"{len(data) // size}{fmt}", data)
),
)
def basic_handler(
self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True
) -> 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
b"".join(self._pack(fmt, value) for value in values)
)
@ -559,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]] = {}
def __init__(
@ -619,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None
self._offset = None
self._offset: int | None = None
def __str__(self) -> str:
return str(dict(self))
def named(self):
def named(self) -> dict[str, Any]:
"""
:returns: dict of name|key: value
@ -638,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __len__(self) -> int:
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
data = self._tagdata[tag]
typ = self.tagtype[tag]
@ -652,10 +657,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __contains__(self, tag: object) -> bool:
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)
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)
info = TiffTags.lookup(tag, self.group)
@ -743,10 +748,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __iter__(self) -> Iterator[int]:
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)
def _pack(self, fmt: str, *values) -> bytes:
def _pack(self, fmt: str, *values: Any) -> bytes:
return struct.pack(self._endian + fmt, *values)
list(
@ -823,7 +828,9 @@ class ImageFileDirectory_v2(_IFDv2Base):
return value
@_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)
def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
@ -848,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise OSError(msg)
return ret
def load(self, fp):
def load(self, fp: IO[bytes]) -> None:
self.reset()
self._offset = fp.tell()
@ -1087,11 +1094,11 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __iter__(self) -> Iterator[int]:
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):
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
data = self._tagdata[tag]
typ = self.tagtype[tag]
@ -1117,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile):
format_description = "Adobe TIFF"
_close_exclusive_fp_after_loading = False
def __init__(self, fp=None, filename=None):
self.tag_v2 = None
def __init__(
self,
fp: StrOrBytesPath | IO[bytes] | None = None,
filename: str | bytes | None = None,
) -> None:
self.tag_v2: ImageFileDirectory_v2
""" Image file directory (tag dictionary) """
self.tag = None
self.tag: ImageFileDirectory_v1
""" Legacy tag entries """
super().__init__(fp, filename)
@ -1136,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile):
self.tag_v2 = ImageFileDirectory_v2(ifh)
# legacy IFD entries will be filled in later
self.ifd: ImageFileDirectory_v1 | None = None
# setup frame pointers
self.__first = self.__next = self.tag_v2.next
self.__frame = -1
@ -1386,11 +1394,14 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("- photometric_interpretation: %s", photo)
logger.debug("- planar_configuration: %s", self._planar_configuration)
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
xsize = int(self.tag_v2.get(IMAGEWIDTH))
ysize = int(self.tag_v2.get(IMAGELENGTH))
xsize = self.tag_v2.get(IMAGEWIDTH)
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
logger.debug("- size: %s", self.size)
@ -1538,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile):
else:
# tiled image
offsets = self.tag_v2[TILEOFFSETS]
w = self.tag_v2.get(TILEWIDTH)
tilewidth = self.tag_v2.get(TILEWIDTH)
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:
if x + w > xsize:
@ -1617,7 +1632,7 @@ SAVE_INFO = {
}
def _save(im, fp, filename):
def _save(im: Image.Image, fp, filename: str | bytes) -> None:
try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e:
@ -1753,10 +1768,11 @@ def _save(im, fp, filename):
if im.mode == "1":
inverted_im = im.copy()
px = inverted_im.load()
for y in range(inverted_im.height):
for x in range(inverted_im.width):
px[x, y] = 0 if px[x, y] == 255 else 255
im = inverted_im
if px is not None:
for y in range(inverted_im.height):
for x in range(inverted_im.width):
px[x, y] = 0 if px[x, y] == 255 else 255
im = inverted_im
else:
im = ImageOps.invert(im)
@ -1798,11 +1814,11 @@ def _save(im, fp, filename):
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
if im.mode == "YCbCr":
for tag, value in {
for tag, default_value in {
YCBCRSUBSAMPLING: (1, 1),
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
}.items():
ifd.setdefault(tag, value)
ifd.setdefault(tag, default_value)
blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
if libtiff:
@ -1845,7 +1861,7 @@ def _save(im, fp, filename):
]
# 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
# the original file, e.g x,y resolution so that we can
# save(load('')) == original file.
@ -1916,13 +1932,15 @@ def _save(im, fp, filename):
offset = ifd.save(fp)
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 --
if "_debug_multipage" in encoderinfo:
# just to access o32 and o16 (using correct byte order)
im._debug_multipage = ifd
setattr(im, "_debug_multipage", ifd)
class AppendingTiffWriter:
@ -2079,38 +2097,34 @@ class AppendingTiffWriter:
(value,) = struct.unpack(self.longFmt, self.f.read(4))
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:
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
self._verify_bytes_written(bytes_written, 4)
def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg)
self._verify_bytes_written(bytes_written, 2)
def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
self._verify_bytes_written(bytes_written, 4)
def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg)
self._verify_bytes_written(bytes_written, 2)
def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
self._verify_bytes_written(bytes_written, 4)
def close(self) -> None:
self.finalize()

View File

@ -45,22 +45,6 @@ class WebPImageFile(ImageFile.ImageFile):
__logical_frame = 0
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,
# and access muxed chunks like ICC/EXIF/XMP.
self._decoder = _webp.WebPAnimDecoder(self.fp.read())
@ -145,21 +129,20 @@ class WebPImageFile(ImageFile.ImageFile):
self._get_next() # Advance to the requested frame
def load(self) -> Image.core.PixelAccess | None:
if _webp.HAVE_WEBPANIM:
if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame)
if self.__loaded != self.__logical_frame:
self._seek(self.__logical_frame)
# We need to load the image data for this frame
data, timestamp, duration = self._get_next()
self.info["timestamp"] = timestamp
self.info["duration"] = duration
self.__loaded = self.__logical_frame
# We need to load the image data for this frame
data, timestamp, duration = self._get_next()
self.info["timestamp"] = timestamp
self.info["duration"] = duration
self.__loaded = self.__logical_frame
# Set tile
if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
# Set tile
if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)
self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
return super().load()
@ -167,9 +150,6 @@ class WebPImageFile(ImageFile.ImageFile):
pass
def tell(self) -> int:
if not _webp.HAVE_WEBPANIM:
return super().tell()
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)
if SUPPORTED:
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_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")
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")

View File

@ -9,6 +9,7 @@ from typing import IO
import PIL
from . import Image
from ._deprecate import deprecate
modules = {
"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)]
features = {
"webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None),
"webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None),
"transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None),
features: dict[str, tuple[str, str | bool, str | None]] = {
"webp_anim": ("PIL._webp", True, None),
"webp_mux": ("PIL._webp", True, None),
"transp_webp": ("PIL._webp", True, None),
"raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"),
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
@ -147,6 +148,9 @@ def check_feature(feature: str) -> bool | None:
try:
imported_module = __import__(module, fromlist=["PIL"])
if isinstance(flag, bool):
deprecate(f'check_feature("{feature}")', 12)
return flag
return getattr(imported_module, flag)
except ModuleNotFoundError:
return None
@ -176,7 +180,17 @@ def get_supported_features() -> list[str]:
"""
: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:
@ -271,9 +285,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
("freetype2", "FREETYPE2"),
("littlecms2", "LITTLECMS2"),
("webp", "WEBP"),
("transp_webp", "WEBP Transparency"),
("webp_mux", "WEBPMUX"),
("webp_anim", "WEBP Animation"),
("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"),

View File

@ -4,8 +4,6 @@
#include <webp/encode.h>
#include <webp/decode.h>
#include <webp/types.h>
#ifdef HAVE_WEBPMUX
#include <webp/mux.h>
#include <webp/demux.h>
@ -13,12 +11,10 @@
* 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
* 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
#define HAVE_WEBPANIM
#endif
#if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107
#error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag
#endif
void
@ -35,8 +31,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie) {
/* WebP Muxer Error Handling */
/* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPMUX
static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = {
"WEBP_MUX_NOT_FOUND",
"WEBP_MUX_INVALID_ARGUMENT",
@ -89,14 +83,10 @@ HandleMuxError(WebPMuxError err, char *chunk) {
return NULL;
}
#endif
/* -------------------------------------------------------------------- */
/* WebP Animation Support */
/* -------------------------------------------------------------------- */
#ifdef HAVE_WEBPANIM
// Encoder type
typedef struct {
PyObject_HEAD WebPAnimEncoder *enc;
@ -576,8 +566,6 @@ static PyTypeObject WebPAnimDecoder_Type = {
0, /*tp_getset*/
};
#endif
/* -------------------------------------------------------------------- */
/* Legacy WebP Support */
/* -------------------------------------------------------------------- */
@ -652,10 +640,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
config.quality = quality_factor;
config.alpha_quality = alpha_quality_factor;
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;
#endif
// Validate the config
if (!WebPValidateConfig(&config)) {
@ -687,19 +672,21 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
WebPPictureFree(&pic);
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;
}
output = writer.mem;
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
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;
}
}
#endif
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
const char *
WebPDecoderVersion_str(void) {
@ -916,85 +780,26 @@ WebPDecoderVersion_str(void) {
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 */
/* -------------------------------------------------------------------- */
static PyMethodDef webpMethods[] = {
#ifdef HAVE_WEBPANIM
{"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"},
{"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"},
#endif
{"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}
};
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
setup_module(PyObject *m) {
#ifdef HAVE_WEBPANIM
/* Ready object types */
if (PyType_Ready(&WebPAnimDecoder_Type) < 0 ||
PyType_Ready(&WebPAnimEncoder_Type) < 0) {
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());
PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None);
Py_XDECREF(v);

View File

@ -314,8 +314,9 @@ test_sorted(PixelList *pl[3]) {
for (i = 0; i < 3; i++) {
l = 256;
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;
}
l = t->p.a.v[i];
}
}
@ -1009,7 +1010,8 @@ compute_palette_from_median_cut(
uint32_t nPixels,
HashTable *medianBoxHash,
Pixel **palette,
uint32_t nPaletteEntries
uint32_t nPaletteEntries,
BoxNode *root
) {
uint32_t i;
uint32_t paletteEntry;
@ -1382,7 +1384,9 @@ quantize(
fflush(stdout);
timer = clock();
#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;
}
#ifdef DEBUG

View File

@ -36,4 +36,4 @@ deps =
extras =
typing
commands =
mypy src Tests {posargs}
mypy docs src winbuild Tests {posargs}

View File

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