diff --git a/.ci/install.sh b/.ci/install.sh index e85e6bdc5..5c20e7f37 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -21,7 +21,7 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ - ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ sway wl-clipboard libopenblas-dev fi diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index c84a3533b..cd1b1a1a1 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1,4 +1,4 @@ -mypy==1.13.0 +mypy==1.14.0 IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index ddb421230..2301a3a7e 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -8,8 +8,8 @@ fi brew install \ freetype \ ghostscript \ + jpeg-turbo \ libimagequant \ - libjpeg \ libtiff \ little-cms2 \ openjpeg \ diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 5f8a4cf78..4e0fad79f 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=10.1.0 LIBPNG_VERSION=1.6.44 -JPEGTURBO_VERSION=3.0.4 +JPEGTURBO_VERSION=3.1.0 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.6.3 TIFF_VERSION=4.6.0 @@ -50,12 +50,8 @@ if [[ -n "$IS_MACOS" ]]; then else GIFLIB_VERSION=5.2.1 fi -if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then - ZLIB_VERSION=1.3.1 -else - ZLIB_VERSION=1.2.8 -fi -LIBWEBP_VERSION=1.4.0 +ZLIB_NG_VERSION=2.2.2 +LIBWEBP_VERSION=1.5.0 BZIP2_VERSION=1.0.8 LIBXCB_VERSION=1.17.0 BROTLI_VERSION=1.1.0 @@ -74,6 +70,16 @@ function build_pkg_config { touch pkg-config-stamp } +function build_zlib_ng { + if [ -e zlib-stamp ]; then return; fi + fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz + (cd zlib-ng-$ZLIB_NG_VERSION \ + && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ + && make -j4 \ + && make install) + touch zlib-stamp +} + function build_brotli { if [ -e brotli-stamp ]; then return; fi local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) @@ -87,7 +93,7 @@ function build_harfbuzz { if [ -e harfbuzz-stamp ]; then return; fi python3 -m pip install meson ninja - local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) + local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) (cd $out_dir \ && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) (cd $out_dir/build \ @@ -100,12 +106,12 @@ function build { if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then yum remove -y zlib-devel fi - build_new_zlib + build_zlib_ng build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist else sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f5..563be0b74 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -34,6 +34,7 @@ def test_wheel_features() -> None: "fribidi", "harfbuzz", "libjpeg_turbo", + "zlib_ng", "xcb", } diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png index 509c42b26..1b58889c8 100644 Binary files a/Tests/images/imagedraw/discontiguous_corners_polygon.png and b/Tests/images/imagedraw/discontiguous_corners_polygon.png differ diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 36ab187f2..baa899df5 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -388,10 +388,12 @@ class TestColorLut3DFilter: table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) lut = ImageFilter.Color3DLUT((5, 6, 7), table) + assert isinstance(lut.table, numpy.ndarray) assert lut.table.shape == (table.size,) # Check application diff --git a/Tests/test_features.py b/Tests/test_features.py index ed7929973..f8f7f6eec 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -36,10 +36,11 @@ def test_version() -> None: else: assert function(name) == version if name != "PIL": - if name == "zlib" and version is not None: - version = re.sub(".zlib-ng$", "", version) - elif name == "libtiff" and version is not None: - version = re.sub("t$", "", version) + if version is not None: + if name == "zlib" and features.check_feature("zlib_ng"): + version = re.sub(".zlib-ng$", "", version) + elif name == "libtiff": + version = re.sub("t$", "", version) assert version is None or re.search(r"\d+(\.\d+)*$", version) for module in features.modules: diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 77ee5b0ea..fc8920317 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - BufrStubImagePlugin._handler = None + BufrStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 237045acc..597ab5083 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -4,8 +4,6 @@ import pytest from PIL import ContainerIO, Image -from .helper import hopper - TEST_FILE = "Tests/images/dummy.container" @@ -15,15 +13,15 @@ def test_sanity() -> None: def test_isatty() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.isatty() is False def test_seekable() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.seekable() is True diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 248347d5b..5d46b157d 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -4,6 +4,7 @@ import warnings from collections.abc import Generator from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -1435,7 +1436,8 @@ def test_saving_rgba(tmp_path: Path) -> None: assert reloaded_rgba.load()[0, 0][3] == 0 -def test_optimizing_p_rgba(tmp_path: Path) -> None: +@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) +def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: out = str(tmp_path / "temp.gif") im1 = Image.new("P", (100, 100)) @@ -1447,7 +1449,7 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None: im2 = Image.new("P", (100, 100)) im2.putpalette(data, "RGBA") - im1.save(out, save_all=True, append_images=[im2]) + im1.save(out, save_all=True, append_images=[im2], **params) with Image.open(out) as reloaded: assert reloaded.n_frames == 2 diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index aba473d24..02e464ff1 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - GribStubImagePlugin._handler = None + GribStubImagePlugin.register_handler(None) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 8275bd0d8..024be9e80 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -85,4 +85,4 @@ def test_handler(tmp_path: Path) -> None: im.save(temp_file) assert handler.saved - Hdf5StubImagePlugin._handler = None + Hdf5StubImagePlugin.register_handler(None) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 347a162a5..5dd50fd08 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1000,8 +1000,13 @@ class TestFileJpeg: with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"XMP test" - im.info["xmp"] = b"1" * 65504 - im.save(f) + # Check that XMP is not saved from image info + reloaded.save(f) + + with Image.open(f) as reloaded: + assert "xmp" not in reloaded.info + + im.save(f, xmp=b"1" * 65504) with Image.open(f) as reloaded: assert reloaded.info["xmp"] == b"1" * 65504 diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 949583185..66fa29177 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -297,3 +297,15 @@ def test_save_all() -> None: # Test that a single frame image will not be saved as an MPO jpg = roundtrip(im, save_all=True) assert "mp" not in jpg.info + + +def test_save_xmp() -> None: + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im2.encoderinfo = {"xmp": b"Second frame"} + im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) + + assert im_reloaded.info["xmp"] == b"First frame" + + im_reloaded.seek(1) + assert im_reloaded.info["xmp"] == b"Second frame" diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 424640d7b..2f1f8cdbc 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -35,6 +35,13 @@ def test_load() -> None: assert im.load()[0, 0] == (255, 255, 255) +def test_load_zero_inch() -> None: + b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10) + with pytest.raises(ValueError): + with Image.open(b): + pass + + def test_register_handler(tmp_path: Path) -> None: class TestHandler(ImageFile.StubHandler): methodCalled = False diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 01bd4b1d7..1181f6fca 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -104,20 +104,20 @@ def test_transposed() -> None: assert im.size == (590, 88) -def test_load_first_unless_jpeg() -> None: +def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: - draft = im.draft + original_draft = im.draft def im_draft( - mode: str, size: tuple[int, int] + mode: str | None, size: tuple[int, int] | None ) -> tuple[str, tuple[int, int, float, float]] | None: - result = draft(mode, size) + result = original_draft(mode, size) assert result is not None return result - im.draft = im_draft + monkeypatch.setattr(im, "draft", im_draft) im.thumbnail((64, 64)) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ed50dfbed..5fc1c2766 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1674,6 +1674,9 @@ def test_continuous_horizontal_edges_polygon() -> None: def test_discontiguous_corners_polygon() -> None: img, draw = create_base_image_draw((84, 68)) draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon( + ((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK + ) draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) draw.polygon( ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 1ee684926..8bef90ce4 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -93,6 +93,19 @@ class TestImageFile: assert p.image is not None assert (48, 48) == p.image.size + @pytest.mark.filterwarnings("ignore:Corrupt EXIF data") + def test_incremental_tiff(self) -> None: + with ImageFile.Parser() as p: + with open("Tests/images/hopper.tif", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + + p.feed(f.read()) + assert p.image is not None + assert (128, 128) == p.image.size + @skip_unless_feature("webp") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index d250ba369..c4f8de013 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -74,6 +74,17 @@ def test_pickle_image( helper_pickle_file(tmp_path, protocol, test_file, test_mode) +def test_pickle_jpeg() -> None: + # Arrange + with Image.open("Tests/images/hopper.jpg") as image: + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) + + # Assert + assert len(unpickled_image.layer) == 3 + assert unpickled_image.layers == 3 + + def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") diff --git a/depends/install_webp.sh b/depends/install_webp.sh index c47fb35f1..9d2977715 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.4.0 +archive=libwebp-1.5.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/reference/features.rst b/docs/reference/features.rst index fcff96735..427c0f606 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,6 +54,7 @@ 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. +* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available. * ``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. diff --git a/docs/releasenotes/11.1.0.rst b/docs/releasenotes/11.1.0.rst new file mode 100644 index 000000000..c5d0afd58 --- /dev/null +++ b/docs/releasenotes/11.1.0.rst @@ -0,0 +1,59 @@ +11.1.0 +------ + +Security +======== + +TODO +^^^^ + +TODO + +:cve:`YYYY-XXXXX`: TODO +^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Check for zlib-ng +^^^^^^^^^^^^^^^^^ + +You can check if Pillow has been built against the zlib-ng version of the +zlib library, and what version of zlib-ng is being used:: + + from PIL import features + features.check_feature("zlib_ng") # True or False + features.version_feature("zlib_ng") # "2.2.2" for example, or None + +Other Changes +============= + +zlib-ng in wheels +^^^^^^^^^^^^^^^^^ + +Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG +was found to be more than twice as fast at higher compression levels. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 641cda4ef..bd8e5536f 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 11.1.0 11.0.0 10.4.0 10.3.0 diff --git a/pyproject.toml b/pyproject.toml index aaaba0032..2c6c7bcd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ test-extras = "tests" [tool.cibuildwheel.macos.environment] PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib" [tool.black] exclude = "wheels/multibuild" diff --git a/setup.py b/setup.py index eea21fa34..a85731db9 100644 --- a/setup.py +++ b/setup.py @@ -344,7 +344,7 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi") ] + [ - ("disable-platform-guessing", None, "Disable platform guessing on Linux"), + ("disable-platform-guessing", None, "Disable platform guessing"), ("debug", None, "Debug logging"), ] + [("add-imaging-libs=", None, "Add libs to _imaging build")] diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index e5605635e..2d03af9d7 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -273,7 +273,7 @@ class BlpImageFile(ImageFile.ImageFile): raise BLPFormatError(msg) self._mode = "RGBA" if self._blp_alpha_depth else "RGB" - self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)] class _BLPBaseDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 1b6408237..9349e2841 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: + struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) ) - ImageFile._save( - im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _accept(prefix: bytes) -> bool: diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index fb1e301c0..36ba15ec5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -454,7 +454,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, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) fp.write(b"\n%%%%EndBinary\n") fp.write(b"grestore end\n") diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 39b4aa552..207d4de4e 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -303,38 +303,38 @@ TAGS = { class GPS(IntEnum): - GPSVersionID = 0 - GPSLatitudeRef = 1 - GPSLatitude = 2 - GPSLongitudeRef = 3 - GPSLongitude = 4 - GPSAltitudeRef = 5 - GPSAltitude = 6 - GPSTimeStamp = 7 - GPSSatellites = 8 - GPSStatus = 9 - GPSMeasureMode = 10 - GPSDOP = 11 - GPSSpeedRef = 12 - GPSSpeed = 13 - GPSTrackRef = 14 - GPSTrack = 15 - GPSImgDirectionRef = 16 - GPSImgDirection = 17 - GPSMapDatum = 18 - GPSDestLatitudeRef = 19 - GPSDestLatitude = 20 - GPSDestLongitudeRef = 21 - GPSDestLongitude = 22 - GPSDestBearingRef = 23 - GPSDestBearing = 24 - GPSDestDistanceRef = 25 - GPSDestDistance = 26 - GPSProcessingMethod = 27 - GPSAreaInformation = 28 - GPSDateStamp = 29 - GPSDifferential = 30 - GPSHPositioningError = 31 + GPSVersionID = 0x00 + GPSLatitudeRef = 0x01 + GPSLatitude = 0x02 + GPSLongitudeRef = 0x03 + GPSLongitude = 0x04 + GPSAltitudeRef = 0x05 + GPSAltitude = 0x06 + GPSTimeStamp = 0x07 + GPSSatellites = 0x08 + GPSStatus = 0x09 + GPSMeasureMode = 0x0A + GPSDOP = 0x0B + GPSSpeedRef = 0x0C + GPSSpeed = 0x0D + GPSTrackRef = 0x0E + GPSTrack = 0x0F + GPSImgDirectionRef = 0x10 + GPSImgDirection = 0x11 + GPSMapDatum = 0x12 + GPSDestLatitudeRef = 0x13 + GPSDestLatitude = 0x14 + GPSDestLongitudeRef = 0x15 + GPSDestLongitude = 0x16 + GPSDestBearingRef = 0x17 + GPSDestBearing = 0x18 + GPSDestDistanceRef = 0x19 + GPSDestDistance = 0x1A + GPSProcessingMethod = 0x1B + GPSAreaInformation = 0x1C + GPSDateStamp = 0x1D + GPSDifferential = 0x1E + GPSHPositioningError = 0x1F """Maps EXIF GPS tags to tag names.""" @@ -342,40 +342,40 @@ GPSTAGS = {i.value: i.name for i in GPS} class Interop(IntEnum): - InteropIndex = 1 - InteropVersion = 2 - RelatedImageFileFormat = 4096 - RelatedImageWidth = 4097 - RelatedImageHeight = 4098 + InteropIndex = 0x0001 + InteropVersion = 0x0002 + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageHeight = 0x1002 class IFD(IntEnum): - Exif = 34665 - GPSInfo = 34853 - Makernote = 37500 - Interop = 40965 + Exif = 0x8769 + GPSInfo = 0x8825 + MakerNote = 0x927C + Interop = 0xA005 IFD1 = -1 class LightSource(IntEnum): - Unknown = 0 - Daylight = 1 - Fluorescent = 2 - Tungsten = 3 - Flash = 4 - Fine = 9 - Cloudy = 10 - Shade = 11 - DaylightFluorescent = 12 - DayWhiteFluorescent = 13 - CoolWhiteFluorescent = 14 - WhiteFluorescent = 15 - StandardLightA = 17 - StandardLightB = 18 - StandardLightC = 19 - D55 = 20 - D65 = 21 - D75 = 22 - D50 = 23 - ISO = 24 - Other = 255 + Unknown = 0x00 + Daylight = 0x01 + Fluorescent = 0x02 + Tungsten = 0x03 + Flash = 0x04 + Fine = 0x09 + Cloudy = 0x0A + Shade = 0x0B + DaylightFluorescent = 0x0C + DayWhiteFluorescent = 0x0D + CoolWhiteFluorescent = 0x0E + WhiteFluorescent = 0x0F + StandardLightA = 0x11 + StandardLightB = 0x12 + StandardLightC = 0x13 + D55 = 0x14 + D65 = 0x15 + D75 = 0x16 + D50 = 0x17 + ISO = 0x18 + Other = 0xFF diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 666390be9..b534b30ab 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)] + self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] self.__offset += framesize diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8fef51076..4cfcb067d 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile): "raw", (x, y, x1, y1), i32(s, i) + 28, - (self.rawmode,), + self.rawmode, ) ) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index ddb469bc3..0516b760c 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile): self._mode = "RGBA" self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] elif format == Format.UNCOMPRESSED: - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] else: msg = f"Invalid texture compression format: {repr(format)}" raise ValueError(msg) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index f1b4969f2..fc4801e9d 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile): "raw", (0, 0) + self.size, 7 + true_color_offset + 4 + 256 * 4, - ("L", 0, 1), + "L", ) ] diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index a7c4f8b2c..47022d584 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -695,8 +695,9 @@ def _write_multiple_frames( ) background = _get_background(im_frame, color) background_im = Image.new("P", im_frame.size, background) - assert im_frames[0].im.palette is not None - background_im.putpalette(im_frames[0].im.palette) + first_palette = im_frames[0].im.palette + assert first_palette is not None + background_im.putpalette(first_palette, first_palette.mode) bbox = _getbbox(background_im, im_frame)[1] elif encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1e289b6c3..90374d804 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -754,7 +754,7 @@ class Image: def __setstate__(self, state: list[Any]) -> None: Image.__init__(self) - info, mode, size, palette, data = state + info, mode, size, palette, data = state[:5] self.info = info self._mode = mode self._size = size @@ -1565,7 +1565,7 @@ class Image: for subifd_offset in subifd_offsets: ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(513): + if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): assert exif._info is not None ifds.append((ifd1, exif._info.next)) @@ -1577,11 +1577,11 @@ class Image: fp = self.fp if ifd is not None: - thumbnail_offset = ifd.get(513) + thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) if thumbnail_offset is not None: thumbnail_offset += getattr(self, "_exif_offset", 0) self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) + data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) fp = io.BytesIO(data) with open(fp) as im: @@ -2556,7 +2556,7 @@ class Image: self._ensure_mutable() save_all = params.pop("save_all", False) - self.encoderinfo = params + self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} self.encoderconfig: tuple[Any, ...] = () preinit() @@ -2603,6 +2603,11 @@ class Image: except PermissionError: pass raise + finally: + try: + del self.encoderinfo + except AttributeError: + pass if open_fp: fp.close() @@ -3884,7 +3889,7 @@ class Exif(_ExifBase): gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) print(gps_ifd) - Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: @@ -4047,11 +4052,11 @@ class Exif(_ExifBase): 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: self.get_ifd(ExifTags.IFD.Exif) tag_data = self._ifds[ExifTags.IFD.Exif][tag] - if tag == ExifTags.IFD.Makernote: + if tag == ExifTags.IFD.MakerNote: from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -4138,7 +4143,7 @@ class Exif(_ExifBase): ifd = { k: v for (k, v) in ifd.items() - if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) } return ifd diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 9836cd688..5d0f87a9f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -98,8 +98,8 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str extents: tuple[int, int, int, int] | None - offset: int - args: tuple[Any, ...] | str | None + offset: int = 0 + args: tuple[Any, ...] | str | None = None # diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 8b0974b2c..b350e56f4 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter): ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size - table = [0] * (size_1d * size_2d * size_3d * ch_out) + table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) idx_in = 0 idx_out = 0 for b in range(size_3d): diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 44aad0c3c..bb29cc0d3 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -698,10 +698,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image 8: Image.Transpose.ROTATE_90, }.get(orientation) if method is not None: - transposed_image = image.transpose(method) if in_place: - image.im = transposed_image.im - image._size = transposed_image._size + image.im = image.im.transpose(method) + image._size = image.im.size + else: + transposed_image = image.transpose(method) exif_image = image if in_place else transposed_image exif = exif_image.getexif() diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 594c56513..068cd5c33 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile): "raw", (0, 0) + self.size, self.fp.tell() - len(buffer), - (self.mode, 0, 1), + self.mode, ) ] diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index ab6a2f497..5025f88ea 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -395,6 +395,13 @@ class JpegImageFile(ImageFile.ImageFile): return getattr(self, "_" + name) raise AttributeError(name) + def __getstate__(self) -> list[Any]: + return super().__getstate__() + [self.layers, self.layer] + + def __setstate__(self, state: list[Any]) -> None: + super().__setstate__(state) + self.layers, self.layer = state[5:] + def load_read(self, read_bytes: int) -> bytes: """ internal: read more image data @@ -751,7 +758,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: extra = info.get("extra", b"") MAX_BYTES_IN_MARKER = 65533 - xmp = info.get("xmp", im.info.get("xmp")) + xmp = info.get("xmp") if xmp: overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index f3460a787..ef6ae87f8 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -70,9 +70,9 @@ class MspImageFile(ImageFile.ImageFile): self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": - self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")] else: - self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)] + self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)] class MspDecoder(ImageFile.PyDecoder): @@ -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, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")]) # diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index e8ea800a4..ac40383f9 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile): self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)] + self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048)] def load_end(self) -> None: if self.tile_post_rotate: diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 36f565f1c..5c465bbdc 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -61,9 +61,7 @@ class PixarImageFile(ImageFile.ImageFile): # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)] # diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 010d3f941..01cc868b2 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile): self._mode = "RGB" if channels == 3 else "RGBA" self.fp.seek(1, os.SEEK_CUR) # colorspace - self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)] + self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())] class QoiDecoder(ImageFile.PyDecoder): diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9f..3a87d009a 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -154,9 +154,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32F" self._mode = "F" - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)] self._fp = self.fp # FIXME: hack @property @@ -211,26 +209,27 @@ class SpiderImageFile(ImageFile.ImageFile): # given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: +def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return None - imglist = [] + byte_imgs = [] for img in filelist: if not os.path.exists(img): print(f"unable to find {img}") continue try: with Image.open(img) as im: - im = im.convert2byte() + assert isinstance(im, SpiderImageFile) + byte_im = im.convert2byte() except Exception: if not isSpiderImage(img): print(f"{img} is not a Spider image file") continue - im.info["filename"] = img - imglist.append(im) - return imglist + byte_im.info["filename"] = img + byte_imgs.append(byte_im) + return byte_imgs # -------------------------------------------------------------------- @@ -280,9 +279,7 @@ 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, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] - ) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9ee55347a..16c521bea 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -935,8 +935,8 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata[tag] = data self.tagtype[tag] = typ - bytes_value = size if size > 32 else repr(data) - msg += f" - value: " + msg += " - value: " + msg += f"" if size > 32 else repr(data) logger.debug(msg) @@ -981,11 +981,8 @@ class ImageFileDirectory_v2(_IFDv2Base): tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") - bytes_value = len(data) if len(data) >= 16 else str(values) - msg = ( - f"save: {tagname} ({tag}) - type: {typname} ({typ})" - f" - value: " - ) + msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: " + msg += f"" if len(data) >= 16 else str(values) logger.debug(msg) # count is sum of lengths for string and arbitrary data @@ -1435,8 +1432,12 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + try: + xsize = self.tag_v2[IMAGEWIDTH] + ysize = self.tag_v2[IMAGELENGTH] + except KeyError as e: + msg = "Missing dimensions" + raise TypeError(msg) from e if not isinstance(xsize, int) or not isinstance(ysize, int): msg = "Invalid dimensions" raise ValueError(msg) @@ -1917,7 +1918,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if not getattr(Image.core, "libtiff_support_custom_tags", False): continue - if tag in ifd.tagtype: + if tag in TiffTags.TAGS_V2_GROUPS: + types[tag] = TiffTags.LONG8 + elif tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] elif not (isinstance(value, (int, float, str, bytes))): continue diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index cad6c98d5..48e9823e8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -92,6 +92,9 @@ class WmfStubImageFile(ImageFile.StubImageFile): # get units per inch self._inch = word(s, 14) + if self._inch == 0: + msg = "Invalid inch" + raise ValueError(msg) # get bounding box x0 = short(s, 6) diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 5d1f201a4..75333354d 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -74,9 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("RGB", PALETTE) self.tile = [ - ImageFile._Tile( - "raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1) - ) + ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode) ] diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index f3d490a84..943a04470 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -67,7 +67,7 @@ class XbmImageFile(ImageFile.ImageFile): self._mode = "1" self._size = xsize, ysize - self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())] def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -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, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)]) fp.write(b"};\n") diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 1fc6c0c39..b985aa5dc 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -101,9 +101,7 @@ class XpmImageFile(ImageFile.ImageFile): self._mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) - self.tile = [ - ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1)) - ] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), "P")] def load_read(self, read_bytes: int) -> bytes: # diff --git a/src/PIL/features.py b/src/PIL/features.py index 75d59e01c..3645e3def 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -127,6 +127,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = { "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"), + "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"), "xcb": ("PIL._imaging", "HAVE_XCB", None), } @@ -308,7 +309,11 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: # this check is also in src/_imagingcms.c:setup_module() version_static = tuple(int(x) for x in v.split(".")) < (2, 7) t = "compiled for" if version_static else "loaded" - if name == "raqm": + if name == "zlib": + zlib_ng_version = version_feature("zlib_ng") + if zlib_ng_version is not None: + v += ", compiled for zlib-ng " + zlib_ng_version + elif name == "raqm": for f in ("fribidi", "harfbuzz"): v2 = version_feature(f) if v2 is not None: diff --git a/src/_imaging.c b/src/_imaging.c index 2db4486b2..5d6d97bed 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4397,6 +4397,20 @@ setup_module(PyObject *m) { } #endif + PyObject *have_zlibng; +#ifdef ZLIBNG_VERSION + have_zlibng = Py_True; + { + PyObject *v = PyUnicode_FromString(ZLIBNG_VERSION); + PyDict_SetItemString(d, "zlib_ng_version", v ? v : Py_None); + Py_XDECREF(v); + } +#else + have_zlibng = Py_False; +#endif + Py_INCREF(have_zlibng); + PyModule_AddObject(m, "HAVE_ZLIBNG", have_zlibng); + #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 1823bcf03..1805ebde1 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -346,10 +346,10 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { return -1; } - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - // transform color channels only - for (i = 0; i < im->ysize; i++) { + // transform color channels only + for (i = 0; i < im->ysize; i++) { cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); } @@ -362,9 +362,9 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { // enough available on all platforms, so we polyfill it here for now. pyCMScopyAux(hTransform, imOut, im); - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return 0; + return 0; } static cmsHTRANSFORM @@ -378,17 +378,17 @@ _buildTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - /* create the transform */ - hTransform = cmsCreateTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - iRenderingIntent, - cmsFLAGS - ); + /* create the transform */ + hTransform = cmsCreateTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + iRenderingIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; @@ -412,19 +412,19 @@ _buildProofTransform( ) { cmsHTRANSFORM hTransform; - Py_BEGIN_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS; - /* create the transform */ - hTransform = cmsCreateProofingTransform( - hInputProfile, - findLCMStype(sInMode), - hOutputProfile, - findLCMStype(sOutMode), - hProofProfile, - iRenderingIntent, - iProofIntent, - cmsFLAGS - ); + /* create the transform */ + hTransform = cmsCreateProofingTransform( + hInputProfile, + findLCMStype(sInMode), + hOutputProfile, + findLCMStype(sOutMode), + hProofProfile, + iRenderingIntent, + iProofIntent, + cmsFLAGS + ); Py_END_ALLOW_THREADS; diff --git a/src/display.c b/src/display.c index b4e2e3899..eed75975d 100644 --- a/src/display.c +++ b/src/display.c @@ -690,24 +690,26 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) { SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); - Py_BEGIN_ALLOW_THREADS ShowWindow(wnd, SW_SHOWNORMAL); + Py_BEGIN_ALLOW_THREADS; + ShowWindow(wnd, SW_SHOWNORMAL); SetForegroundWindow(wnd); /* to make sure it's visible */ - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - return Py_BuildValue(F_HANDLE, wnd); + return Py_BuildValue(F_HANDLE, wnd); } PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { MSG msg; - Py_BEGIN_ALLOW_THREADS while (mainloop && GetMessage(&msg, NULL, 0, 0)) { + Py_BEGIN_ALLOW_THREADS; + while (mainloop && GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } - Py_END_ALLOW_THREADS + Py_END_ALLOW_THREADS; - Py_INCREF(Py_None); + Py_INCREF(Py_None); return Py_None; } diff --git a/src/encode.c b/src/encode.c index 1a4cd489d..d369a1b45 100644 --- a/src/encode.c +++ b/src/encode.c @@ -736,7 +736,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { } if (tag_type) { int type_int = PyLong_AsLong(tag_type); - if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { + if (type_int >= TIFF_BYTE && type_int <= TIFF_LONG8) { type = (TIFFDataType)type_int; } } @@ -929,7 +929,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { ); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value) + &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value) ); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( @@ -959,6 +959,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) ); + } else if (type == TIFF_LONG8) { + status = ImagingLibTiffSetField( + &encoder->state, (ttag_t)key_int, (uint64_t)PyLong_AsLongLong(value) + ); } else { TRACE( ("Unhandled type for key %d : %s \n", diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index f1c8ffcff..ea6f8805e 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -501,7 +501,8 @@ polygon_generic( // Needed to draw consistent polygons xx[j] = xx[j - 1]; j++; - } else if (current->dx != 0 && roundf(xx[j - 1]) == xx[j - 1]) { + } else if (current->dx != 0 && j % 2 == 1 && + roundf(xx[j - 1]) == xx[j - 1]) { // Connect discontiguous corners for (k = 0; k < i; k++) { Edge *other_edge = edge_table[k]; @@ -510,10 +511,8 @@ polygon_generic( continue; } // Check if the two edges join to make a corner - if (((ymin == current->ymin && ymin == other_edge->ymin) || - (ymin == current->ymax && ymin == other_edge->ymax)) && - xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx + - other_edge->x0) { + if (xx[j - 1] == + (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) { // Determine points from the edges on the next row // Or if this is the last row, check the previous row int offset = ymin == ymax ? -1 : 1; diff --git a/wheels/multibuild b/wheels/multibuild index 9a9d1275f..42d761728 160000 --- a/wheels/multibuild +++ b/wheels/multibuild @@ -1 +1 @@ -Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 +Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1efe0dcac..0674a9a15 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +import sys from typing import Any @@ -113,26 +114,24 @@ V = { "FREETYPE": "2.13.3", "FRIBIDI": "1.0.16", "HARFBUZZ": "10.1.0", - "JPEGTURBO": "3.0.4", + "JPEGTURBO": "3.1.0", "LCMS2": "2.16", "LIBPNG": "1.6.44", - "LIBWEBP": "1.4.0", + "LIBWEBP": "1.5.0", "OPENJPEG": "2.5.3", "TIFF": "4.6.0", "XZ": "5.6.3", - "ZLIB": "1.3.1", + "ZLIBNG": "2.2.2", } V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) -V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation DEPS: dict[str, dict[str, Any]] = { "libjpeg": { - "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/FILENAME/download", + "url": f"https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/{V['JPEGTURBO']}/libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz", - "dir": f"libjpeg-turbo-{V['JPEGTURBO']}", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" @@ -155,28 +154,30 @@ DEPS: dict[str, dict[str, Any]] = { cmd_copy("cjpeg-static.exe", "cjpeg.exe"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], - "headers": ["j*.h"], + "headers": ["jconfig.h", r"src\j*.h"], "libs": ["libjpeg.lib"], "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/FILENAME", - "filename": f"zlib{V['ZLIB_DOTLESS']}.zip", - "dir": f"zlib-{V['ZLIB']}", - "license": "README", - "license_pattern": "Copyright notice:\n\n(.+)$", + "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz", + "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz", + "license": "LICENSE.md", + "patch": { + r"CMakeLists.txt": { + "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlib)", # noqa: E501 + }, + }, "build": [ - cmd_nmake(r"win32\Makefile.msc", "clean"), - cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), - cmd_copy("zlib.lib", "z.lib"), + *cmds_cmake( + "zlib", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON" + ), ], "headers": [r"z*.h"], - "libs": [r"*.lib"], + "libs": [r"zlib.lib"], }, "xz": { "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "filename": f"xz-{V['XZ']}.tar.gz", - "dir": f"xz-{V['XZ']}", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -189,7 +190,6 @@ DEPS: dict[str, dict[str, Any]] = { "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/FILENAME", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz", - "dir": f"libwebp-{V['LIBWEBP']}", "license": "COPYING", "patch": { r"src\enc\picture_csp_enc.c": { @@ -211,7 +211,6 @@ DEPS: dict[str, dict[str, Any]] = { "libtiff": { "url": "https://download.osgeo.org/libtiff/FILENAME", "filename": f"tiff-{V['TIFF']}.tar.gz", - "dir": f"tiff-{V['TIFF']}", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -244,7 +243,6 @@ DEPS: dict[str, dict[str, Any]] = { "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/" f"lpng{V['LIBPNG_DOTLESS']}.zip/download", "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", - "dir": f"lpng{V['LIBPNG_DOTLESS']}", "license": "LICENSE", "build": [ *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), @@ -258,7 +256,6 @@ DEPS: dict[str, dict[str, Any]] = { "brotli": { "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", "filename": f"brotli-{V['BROTLI']}.tar.gz", - "dir": f"brotli-{V['BROTLI']}", "license": "LICENSE", "build": [ *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -269,7 +266,6 @@ DEPS: dict[str, dict[str, Any]] = { "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "filename": f"freetype-{V['FREETYPE']}.tar.gz", - "dir": f"freetype-{V['FREETYPE']}", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { @@ -304,7 +300,6 @@ DEPS: dict[str, dict[str, Any]] = { "lcms2": { "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "filename": f"lcms2-{V['LCMS2']}.tar.gz", - "dir": f"lcms2-{V['LCMS2']}", "license": "LICENSE", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { @@ -330,7 +325,6 @@ DEPS: dict[str, dict[str, Any]] = { "openjpeg": { "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", - "dir": f"openjpeg-{V['OPENJPEG']}", "license": "LICENSE", "build": [ *cmds_cmake( @@ -345,7 +339,6 @@ DEPS: dict[str, dict[str, Any]] = { # commit: Merge branch 'master' into msvc (matches 2.17.0 tag) "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", - "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", "license": "COPYRIGHT", "patch": { "CMakeLists.txt": { @@ -365,7 +358,6 @@ DEPS: dict[str, dict[str, Any]] = { "harfbuzz": { "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", "filename": f"harfbuzz-{V['HARFBUZZ']}.zip", - "dir": f"harfbuzz-{V['HARFBUZZ']}", "license": "COPYING", "build": [ *cmds_cmake( @@ -380,7 +372,6 @@ DEPS: dict[str, dict[str, Any]] = { "fribidi": { "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", "filename": f"fribidi-{V['FRIBIDI']}.zip", - "dir": f"fribidi-{V['FRIBIDI']}", "license": "COPYING", "build": [ cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), @@ -517,7 +508,10 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None: if sources_dir_abs != member_prefix: msg = "Attempted Path Traversal in Tar File" raise RuntimeError(msg) - tgz.extractall(sources_dir) + if sys.version_info >= (3, 12): + tgz.extractall(sources_dir, filter="data") + else: + tgz.extractall(sources_dir) else: msg = "Unknown archive type: " + filename raise RuntimeError(msg) @@ -760,6 +754,8 @@ def main() -> None: } for k, v in DEPS.items(): + if "dir" not in v: + v["dir"] = re.sub(r"\.(tar\.gz|zip)", "", v["filename"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"]) print()