Merge branch 'main' into jpeg2000_cmyk_save

This commit is contained in:
Andrew Murray 2024-12-28 07:42:56 +11:00 committed by GitHub
commit 439d5cf2a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 387 additions and 235 deletions

View File

@ -21,7 +21,7 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ 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\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev sway wl-clipboard libopenblas-dev
fi fi

View File

@ -1,4 +1,4 @@
mypy==1.13.0 mypy==1.14.0
IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6 IceSpringPySideStubs-PySide6
ipython ipython

View File

@ -8,8 +8,8 @@ fi
brew install \ brew install \
freetype \ freetype \
ghostscript \ ghostscript \
jpeg-turbo \
libimagequant \ libimagequant \
libjpeg \
libtiff \ libtiff \
little-cms2 \ little-cms2 \
openjpeg \ openjpeg \

View File

@ -40,7 +40,7 @@ ARCHIVE_SDIR=pillow-depends-main
FREETYPE_VERSION=2.13.2 FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=10.1.0 HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44 LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.0.4 JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3 OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3 XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0 TIFF_VERSION=4.6.0
@ -50,12 +50,8 @@ if [[ -n "$IS_MACOS" ]]; then
else else
GIFLIB_VERSION=5.2.1 GIFLIB_VERSION=5.2.1
fi fi
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then ZLIB_NG_VERSION=2.2.2
ZLIB_VERSION=1.3.1 LIBWEBP_VERSION=1.5.0
else
ZLIB_VERSION=1.2.8
fi
LIBWEBP_VERSION=1.4.0
BZIP2_VERSION=1.0.8 BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0 BROTLI_VERSION=1.1.0
@ -74,6 +70,16 @@ function build_pkg_config {
touch pkg-config-stamp 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 { function build_brotli {
if [ -e brotli-stamp ]; then return; fi 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) 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 if [ -e harfbuzz-stamp ]; then return; fi
python3 -m pip install meson ninja 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 \ (cd $out_dir \
&& meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled)
(cd $out_dir/build \ (cd $out_dir/build \
@ -100,12 +106,12 @@ function build {
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel yum remove -y zlib-devel
fi fi
build_new_zlib build_zlib_ng
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto 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 build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
else else
sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc

View File

@ -34,6 +34,7 @@ def test_wheel_features() -> None:
"fribidi", "fribidi",
"harfbuzz", "harfbuzz",
"libjpeg_turbo", "libjpeg_turbo",
"zlib_ng",
"xcb", "xcb",
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 533 B

View File

@ -388,10 +388,12 @@ class TestColorLut3DFilter:
table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16)
lut = ImageFilter.Color3DLUT((5, 6, 7), table) lut = ImageFilter.Color3DLUT((5, 6, 7), table)
assert isinstance(lut.table, numpy.ndarray)
assert lut.table.shape == (table.size,) assert lut.table.shape == (table.size,)
table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16)
lut = ImageFilter.Color3DLUT((5, 6, 7), table) lut = ImageFilter.Color3DLUT((5, 6, 7), table)
assert isinstance(lut.table, numpy.ndarray)
assert lut.table.shape == (table.size,) assert lut.table.shape == (table.size,)
# Check application # Check application

View File

@ -36,9 +36,10 @@ def test_version() -> None:
else: else:
assert function(name) == version assert function(name) == version
if name != "PIL": if name != "PIL":
if name == "zlib" and version is not None: if version is not None:
if name == "zlib" and features.check_feature("zlib_ng"):
version = re.sub(".zlib-ng$", "", version) version = re.sub(".zlib-ng$", "", version)
elif name == "libtiff" and version is not None: elif name == "libtiff":
version = re.sub("t$", "", version) version = re.sub("t$", "", version)
assert version is None or re.search(r"\d+(\.\d+)*$", version) assert version is None or re.search(r"\d+(\.\d+)*$", version)

View File

@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None:
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved
BufrStubImagePlugin._handler = None BufrStubImagePlugin.register_handler(None)

View File

@ -4,8 +4,6 @@ import pytest
from PIL import ContainerIO, Image from PIL import ContainerIO, Image
from .helper import hopper
TEST_FILE = "Tests/images/dummy.container" TEST_FILE = "Tests/images/dummy.container"
@ -15,15 +13,15 @@ def test_sanity() -> None:
def test_isatty() -> None: def test_isatty() -> None:
with hopper() as im: with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(im, 0, 0) container = ContainerIO.ContainerIO(fh, 0, 0)
assert container.isatty() is False assert container.isatty() is False
def test_seekable() -> None: def test_seekable() -> None:
with hopper() as im: with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(im, 0, 0) container = ContainerIO.ContainerIO(fh, 0, 0)
assert container.seekable() is True assert container.seekable() is True

View File

@ -4,6 +4,7 @@ import warnings
from collections.abc import Generator from collections.abc import Generator
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -1435,7 +1436,8 @@ def test_saving_rgba(tmp_path: Path) -> None:
assert reloaded_rgba.load()[0, 0][3] == 0 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") out = str(tmp_path / "temp.gif")
im1 = Image.new("P", (100, 100)) 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 = Image.new("P", (100, 100))
im2.putpalette(data, "RGBA") 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: with Image.open(out) as reloaded:
assert reloaded.n_frames == 2 assert reloaded.n_frames == 2

View File

@ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None:
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved
GribStubImagePlugin._handler = None GribStubImagePlugin.register_handler(None)

View File

@ -85,4 +85,4 @@ def test_handler(tmp_path: Path) -> None:
im.save(temp_file) im.save(temp_file)
assert handler.saved assert handler.saved
Hdf5StubImagePlugin._handler = None Hdf5StubImagePlugin.register_handler(None)

View File

@ -1000,8 +1000,13 @@ class TestFileJpeg:
with Image.open(f) as reloaded: with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test" assert reloaded.info["xmp"] == b"XMP test"
im.info["xmp"] = b"1" * 65504 # Check that XMP is not saved from image info
im.save(f) 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: with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504 assert reloaded.info["xmp"] == b"1" * 65504

View File

@ -297,3 +297,15 @@ def test_save_all() -> None:
# Test that a single frame image will not be saved as an MPO # Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True) jpg = roundtrip(im, save_all=True)
assert "mp" not in jpg.info 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"

View File

@ -35,6 +35,13 @@ def test_load() -> None:
assert im.load()[0, 0] == (255, 255, 255) 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: def test_register_handler(tmp_path: Path) -> None:
class TestHandler(ImageFile.StubHandler): class TestHandler(ImageFile.StubHandler):
methodCalled = False methodCalled = False

View File

@ -104,20 +104,20 @@ def test_transposed() -> None:
assert im.size == (590, 88) 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 # Test that thumbnail() still uses draft() for JPEG
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft original_draft = im.draft
def 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: ) -> tuple[str, tuple[int, int, float, float]] | None:
result = draft(mode, size) result = original_draft(mode, size)
assert result is not None assert result is not None
return result return result
im.draft = im_draft monkeypatch.setattr(im, "draft", im_draft)
im.thumbnail((64, 64)) im.thumbnail((64, 64))

View File

@ -1674,6 +1674,9 @@ def test_continuous_horizontal_edges_polygon() -> None:
def test_discontiguous_corners_polygon() -> None: def test_discontiguous_corners_polygon() -> None:
img, draw = create_base_image_draw((84, 68)) img, draw = create_base_image_draw((84, 68))
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) 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(((71, 44), (38, 27), (1, 24)), BLACK)
draw.polygon( draw.polygon(
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),

View File

@ -93,6 +93,19 @@ class TestImageFile:
assert p.image is not None assert p.image is not None
assert (48, 48) == p.image.size 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") @skip_unless_feature("webp")
def test_incremental_webp(self) -> None: def test_incremental_webp(self) -> None:
with ImageFile.Parser() as p: with ImageFile.Parser() as p:

View File

@ -74,6 +74,17 @@ def test_pickle_image(
helper_pickle_file(tmp_path, protocol, test_file, test_mode) 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: def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
# Arrange # Arrange
filename = str(tmp_path / "temp.pkl") filename = str(tmp_path / "temp.pkl")

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install webp # 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 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -54,6 +54,7 @@ Feature version numbers are available only where stated.
Support for the following features can be checked: Support for the following features can be checked:
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
* ``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. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
* ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library.

View File

@ -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.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
11.1.0
11.0.0 11.0.0
10.4.0 10.4.0
10.3.0 10.3.0

View File

@ -104,6 +104,7 @@ test-extras = "tests"
[tool.cibuildwheel.macos.environment] [tool.cibuildwheel.macos.environment]
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" 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] [tool.black]
exclude = "wheels/multibuild" exclude = "wheels/multibuild"

View File

@ -344,7 +344,7 @@ class pil_build_ext(build_ext):
for x in ("raqm", "fribidi") 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"), ("debug", None, "Debug logging"),
] ]
+ [("add-imaging-libs=", None, "Add libs to _imaging build")] + [("add-imaging-libs=", None, "Add libs to _imaging build")]

View File

@ -273,7 +273,7 @@ class BlpImageFile(ImageFile.ImageFile):
raise BLPFormatError(msg) raise BLPFormatError(msg)
self._mode = "RGBA" if self._blp_alpha_depth else "RGB" 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): class _BLPBaseDecoder(ImageFile.PyDecoder):

View File

@ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ struct.pack("<4I", *rgba_mask) # dwRGBABitMask + struct.pack("<4I", *rgba_mask) # dwRGBABitMask
+ struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0)
) )
ImageFile._save( ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:

View File

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

View File

@ -303,38 +303,38 @@ TAGS = {
class GPS(IntEnum): class GPS(IntEnum):
GPSVersionID = 0 GPSVersionID = 0x00
GPSLatitudeRef = 1 GPSLatitudeRef = 0x01
GPSLatitude = 2 GPSLatitude = 0x02
GPSLongitudeRef = 3 GPSLongitudeRef = 0x03
GPSLongitude = 4 GPSLongitude = 0x04
GPSAltitudeRef = 5 GPSAltitudeRef = 0x05
GPSAltitude = 6 GPSAltitude = 0x06
GPSTimeStamp = 7 GPSTimeStamp = 0x07
GPSSatellites = 8 GPSSatellites = 0x08
GPSStatus = 9 GPSStatus = 0x09
GPSMeasureMode = 10 GPSMeasureMode = 0x0A
GPSDOP = 11 GPSDOP = 0x0B
GPSSpeedRef = 12 GPSSpeedRef = 0x0C
GPSSpeed = 13 GPSSpeed = 0x0D
GPSTrackRef = 14 GPSTrackRef = 0x0E
GPSTrack = 15 GPSTrack = 0x0F
GPSImgDirectionRef = 16 GPSImgDirectionRef = 0x10
GPSImgDirection = 17 GPSImgDirection = 0x11
GPSMapDatum = 18 GPSMapDatum = 0x12
GPSDestLatitudeRef = 19 GPSDestLatitudeRef = 0x13
GPSDestLatitude = 20 GPSDestLatitude = 0x14
GPSDestLongitudeRef = 21 GPSDestLongitudeRef = 0x15
GPSDestLongitude = 22 GPSDestLongitude = 0x16
GPSDestBearingRef = 23 GPSDestBearingRef = 0x17
GPSDestBearing = 24 GPSDestBearing = 0x18
GPSDestDistanceRef = 25 GPSDestDistanceRef = 0x19
GPSDestDistance = 26 GPSDestDistance = 0x1A
GPSProcessingMethod = 27 GPSProcessingMethod = 0x1B
GPSAreaInformation = 28 GPSAreaInformation = 0x1C
GPSDateStamp = 29 GPSDateStamp = 0x1D
GPSDifferential = 30 GPSDifferential = 0x1E
GPSHPositioningError = 31 GPSHPositioningError = 0x1F
"""Maps EXIF GPS tags to tag names.""" """Maps EXIF GPS tags to tag names."""
@ -342,40 +342,40 @@ GPSTAGS = {i.value: i.name for i in GPS}
class Interop(IntEnum): class Interop(IntEnum):
InteropIndex = 1 InteropIndex = 0x0001
InteropVersion = 2 InteropVersion = 0x0002
RelatedImageFileFormat = 4096 RelatedImageFileFormat = 0x1000
RelatedImageWidth = 4097 RelatedImageWidth = 0x1001
RelatedImageHeight = 4098 RelatedImageHeight = 0x1002
class IFD(IntEnum): class IFD(IntEnum):
Exif = 34665 Exif = 0x8769
GPSInfo = 34853 GPSInfo = 0x8825
Makernote = 37500 MakerNote = 0x927C
Interop = 40965 Interop = 0xA005
IFD1 = -1 IFD1 = -1
class LightSource(IntEnum): class LightSource(IntEnum):
Unknown = 0 Unknown = 0x00
Daylight = 1 Daylight = 0x01
Fluorescent = 2 Fluorescent = 0x02
Tungsten = 3 Tungsten = 0x03
Flash = 4 Flash = 0x04
Fine = 9 Fine = 0x09
Cloudy = 10 Cloudy = 0x0A
Shade = 11 Shade = 0x0B
DaylightFluorescent = 12 DaylightFluorescent = 0x0C
DayWhiteFluorescent = 13 DayWhiteFluorescent = 0x0D
CoolWhiteFluorescent = 14 CoolWhiteFluorescent = 0x0E
WhiteFluorescent = 15 WhiteFluorescent = 0x0F
StandardLightA = 17 StandardLightA = 0x11
StandardLightB = 18 StandardLightB = 0x12
StandardLightC = 19 StandardLightC = 0x13
D55 = 20 D55 = 0x14
D65 = 21 D65 = 0x15
D75 = 22 D75 = 0x16
D50 = 23 D50 = 0x17
ISO = 24 ISO = 0x18
Other = 255 Other = 0xFF

View File

@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile):
framesize = i32(s) framesize = i32(s)
self.decodermaxblock = framesize 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 self.__offset += framesize

View File

@ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile):
"raw", "raw",
(x, y, x1, y1), (x, y, x1, y1),
i32(s, i) + 28, i32(s, i) + 28,
(self.rawmode,), self.rawmode,
) )
) )

View File

@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile):
self._mode = "RGBA" self._mode = "RGBA"
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
elif format == Format.UNCOMPRESSED: 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: else:
msg = f"Invalid texture compression format: {repr(format)}" msg = f"Invalid texture compression format: {repr(format)}"
raise ValueError(msg) raise ValueError(msg)

View File

@ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile):
"raw", "raw",
(0, 0) + self.size, (0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4, 7 + true_color_offset + 4 + 256 * 4,
("L", 0, 1), "L",
) )
] ]

View File

@ -695,8 +695,9 @@ def _write_multiple_frames(
) )
background = _get_background(im_frame, color) background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background) background_im = Image.new("P", im_frame.size, background)
assert im_frames[0].im.palette is not None first_palette = im_frames[0].im.palette
background_im.putpalette(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] bbox = _getbbox(background_im, im_frame)[1]
elif encoderinfo.get("optimize") and im_frame.mode != "1": elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo: if "transparency" not in encoderinfo:

View File

@ -754,7 +754,7 @@ class Image:
def __setstate__(self, state: list[Any]) -> None: def __setstate__(self, state: list[Any]) -> None:
Image.__init__(self) Image.__init__(self)
info, mode, size, palette, data = state info, mode, size, palette, data = state[:5]
self.info = info self.info = info
self._mode = mode self._mode = mode
self._size = size self._size = size
@ -1565,7 +1565,7 @@ class Image:
for subifd_offset in subifd_offsets: for subifd_offset in subifd_offsets:
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
if ifd1 and ifd1.get(513): if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
assert exif._info is not None assert exif._info is not None
ifds.append((ifd1, exif._info.next)) ifds.append((ifd1, exif._info.next))
@ -1577,11 +1577,11 @@ class Image:
fp = self.fp fp = self.fp
if ifd is not None: if ifd is not None:
thumbnail_offset = ifd.get(513) thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
if thumbnail_offset is not None: if thumbnail_offset is not None:
thumbnail_offset += getattr(self, "_exif_offset", 0) thumbnail_offset += getattr(self, "_exif_offset", 0)
self.fp.seek(thumbnail_offset) 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) fp = io.BytesIO(data)
with open(fp) as im: with open(fp) as im:
@ -2556,7 +2556,7 @@ class Image:
self._ensure_mutable() self._ensure_mutable()
save_all = params.pop("save_all", False) save_all = params.pop("save_all", False)
self.encoderinfo = params self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}
self.encoderconfig: tuple[Any, ...] = () self.encoderconfig: tuple[Any, ...] = ()
preinit() preinit()
@ -2603,6 +2603,11 @@ class Image:
except PermissionError: except PermissionError:
pass pass
raise raise
finally:
try:
del self.encoderinfo
except AttributeError:
pass
if open_fp: if open_fp:
fp.close() fp.close()
@ -3884,7 +3889,7 @@ class Exif(_ExifBase):
gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo)
print(gps_ifd) 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``. ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``.
:py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: :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) ifd = self._get_ifd_dict(offset, tag)
if ifd is not None: if ifd is not None:
self._ifds[tag] = ifd self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]:
if ExifTags.IFD.Exif not in self._ifds: if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif) self.get_ifd(ExifTags.IFD.Exif)
tag_data = self._ifds[ExifTags.IFD.Exif][tag] tag_data = self._ifds[ExifTags.IFD.Exif][tag]
if tag == ExifTags.IFD.Makernote: if tag == ExifTags.IFD.MakerNote:
from .TiffImagePlugin import ImageFileDirectory_v2 from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data[:8] == b"FUJIFILM": if tag_data[:8] == b"FUJIFILM":
@ -4138,7 +4143,7 @@ class Exif(_ExifBase):
ifd = { ifd = {
k: v k: v
for (k, v) in ifd.items() 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 return ifd

View File

@ -98,8 +98,8 @@ def _tilesort(t: _Tile) -> int:
class _Tile(NamedTuple): class _Tile(NamedTuple):
codec_name: str codec_name: str
extents: tuple[int, int, int, int] | None extents: tuple[int, int, int, int] | None
offset: int offset: int = 0
args: tuple[Any, ...] | str | None args: tuple[Any, ...] | str | None = None
# #

View File

@ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter):
ch_out = channels or ch_in ch_out = channels or ch_in
size_1d, size_2d, size_3d = self.size 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_in = 0
idx_out = 0 idx_out = 0
for b in range(size_3d): for b in range(size_3d):

View File

@ -698,10 +698,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image
8: Image.Transpose.ROTATE_90, 8: Image.Transpose.ROTATE_90,
}.get(orientation) }.get(orientation)
if method is not None: if method is not None:
transposed_image = image.transpose(method)
if in_place: if in_place:
image.im = transposed_image.im image.im = image.im.transpose(method)
image._size = transposed_image._size image._size = image.im.size
else:
transposed_image = image.transpose(method)
exif_image = image if in_place else transposed_image exif_image = image if in_place else transposed_image
exif = exif_image.getexif() exif = exif_image.getexif()

View File

@ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile):
"raw", "raw",
(0, 0) + self.size, (0, 0) + self.size,
self.fp.tell() - len(buffer), self.fp.tell() - len(buffer),
(self.mode, 0, 1), self.mode,
) )
] ]

View File

@ -395,6 +395,13 @@ class JpegImageFile(ImageFile.ImageFile):
return getattr(self, "_" + name) return getattr(self, "_" + name)
raise AttributeError(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: def load_read(self, read_bytes: int) -> bytes:
""" """
internal: read more image data 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"") extra = info.get("extra", b"")
MAX_BYTES_IN_MARKER = 65533 MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp", im.info.get("xmp")) xmp = info.get("xmp")
if xmp: if xmp:
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00" overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len

View File

@ -70,9 +70,9 @@ class MspImageFile(ImageFile.ImageFile):
self._size = i16(s, 4), i16(s, 6) self._size = i16(s, 4), i16(s, 6)
if s[:4] == b"DanM": 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: 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): class MspDecoder(ImageFile.PyDecoder):
@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(o16(h)) fp.write(o16(h))
# image body # image body
ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")])
# #

View File

@ -47,7 +47,7 @@ class PcdImageFile(ImageFile.ImageFile):
self._mode = "RGB" self._mode = "RGB"
self._size = 768, 512 # FIXME: not correct for rotated images! 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: def load_end(self) -> None:
if self.tile_post_rotate: if self.tile_post_rotate:

View File

@ -61,9 +61,7 @@ class PixarImageFile(ImageFile.ImageFile):
# FIXME: to be continued... # FIXME: to be continued...
# create tile descriptor (assuming "dumped") # create tile descriptor (assuming "dumped")
self.tile = [ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)]
ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))
]
# #

View File

@ -32,7 +32,7 @@ class QoiImageFile(ImageFile.ImageFile):
self._mode = "RGB" if channels == 3 else "RGBA" self._mode = "RGB" if channels == 3 else "RGBA"
self.fp.seek(1, os.SEEK_CUR) # colorspace 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): class QoiDecoder(ImageFile.PyDecoder):

View File

@ -154,9 +154,7 @@ class SpiderImageFile(ImageFile.ImageFile):
self.rawmode = "F;32F" self.rawmode = "F;32F"
self._mode = "F" self._mode = "F"
self.tile = [ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)]
ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))
]
self._fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
@property @property
@ -211,26 +209,27 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images # 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""" """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1: if filelist is None or len(filelist) < 1:
return None return None
imglist = [] byte_imgs = []
for img in filelist: for img in filelist:
if not os.path.exists(img): if not os.path.exists(img):
print(f"unable to find {img}") print(f"unable to find {img}")
continue continue
try: try:
with Image.open(img) as im: with Image.open(img) as im:
im = im.convert2byte() assert isinstance(im, SpiderImageFile)
byte_im = im.convert2byte()
except Exception: except Exception:
if not isSpiderImage(img): if not isSpiderImage(img):
print(f"{img} is not a Spider image file") print(f"{img} is not a Spider image file")
continue continue
im.info["filename"] = img byte_im.info["filename"] = img
imglist.append(im) byte_imgs.append(byte_im)
return imglist return byte_imgs
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -280,9 +279,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.writelines(hdr) fp.writelines(hdr)
rawmode = "F;32NF" # 32-bit native floating point rawmode = "F;32NF" # 32-bit native floating point
ImageFile._save( ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]
)
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:

View File

@ -935,8 +935,8 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tagdata[tag] = data self._tagdata[tag] = data
self.tagtype[tag] = typ self.tagtype[tag] = typ
bytes_value = size if size > 32 else repr(data) msg += " - value: "
msg += f" - value: <table: {bytes_value} bytes>" msg += f"<table: {size} bytes>" if size > 32 else repr(data)
logger.debug(msg) logger.debug(msg)
@ -981,11 +981,8 @@ class ImageFileDirectory_v2(_IFDv2Base):
tagname = TiffTags.lookup(tag, self.group).name tagname = TiffTags.lookup(tag, self.group).name
typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") 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}) - value: "
msg = ( msg += f"<table: {len(data)} bytes>" if len(data) >= 16 else str(values)
f"save: {tagname} ({tag}) - type: {typname} ({typ})"
f" - value: <table: {bytes_value} bytes>"
)
logger.debug(msg) logger.debug(msg)
# count is sum of lengths for string and arbitrary data # 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)) logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
# size # size
xsize = self.tag_v2.get(IMAGEWIDTH) try:
ysize = self.tag_v2.get(IMAGELENGTH) 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): if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions" msg = "Invalid dimensions"
raise ValueError(msg) 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): if not getattr(Image.core, "libtiff_support_custom_tags", False):
continue 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] types[tag] = ifd.tagtype[tag]
elif not (isinstance(value, (int, float, str, bytes))): elif not (isinstance(value, (int, float, str, bytes))):
continue continue

View File

@ -92,6 +92,9 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# get units per inch # get units per inch
self._inch = word(s, 14) self._inch = word(s, 14)
if self._inch == 0:
msg = "Invalid inch"
raise ValueError(msg)
# get bounding box # get bounding box
x0 = short(s, 6) x0 = short(s, 6)

View File

@ -74,9 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile):
self.palette = ImagePalette.raw("RGB", PALETTE) self.palette = ImagePalette.raw("RGB", PALETTE)
self.tile = [ self.tile = [
ImageFile._Tile( ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode)
"raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1)
)
] ]

View File

@ -67,7 +67,7 @@ class XbmImageFile(ImageFile.ImageFile):
self._mode = "1" self._mode = "1"
self._size = xsize, ysize 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: 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") 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") fp.write(b"};\n")

View File

@ -101,9 +101,7 @@ class XpmImageFile(ImageFile.ImageFile):
self._mode = "P" self._mode = "P"
self.palette = ImagePalette.raw("RGB", b"".join(palette)) self.palette = ImagePalette.raw("RGB", b"".join(palette))
self.tile = [ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), "P")]
ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))
]
def load_read(self, read_bytes: int) -> bytes: def load_read(self, read_bytes: int) -> bytes:
# #

View File

@ -127,6 +127,7 @@ features: dict[str, tuple[str, str | bool, str | None]] = {
"fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
"harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
"libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_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"), "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
"xcb": ("PIL._imaging", "HAVE_XCB", None), "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() # this check is also in src/_imagingcms.c:setup_module()
version_static = tuple(int(x) for x in v.split(".")) < (2, 7) version_static = tuple(int(x) for x in v.split(".")) < (2, 7)
t = "compiled for" if version_static else "loaded" 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"): for f in ("fribidi", "harfbuzz"):
v2 = version_feature(f) v2 = version_feature(f)
if v2 is not None: if v2 is not None:

View File

@ -4397,6 +4397,20 @@ setup_module(PyObject *m) {
} }
#endif #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 #ifdef HAVE_LIBTIFF
{ {
extern const char *ImagingTiffVersion(void); extern const char *ImagingTiffVersion(void);

View File

@ -346,7 +346,7 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) {
return -1; return -1;
} }
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS;
// transform color channels only // transform color channels only
for (i = 0; i < im->ysize; i++) { for (i = 0; i < im->ysize; i++) {
@ -362,7 +362,7 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) {
// enough available on all platforms, so we polyfill it here for now. // enough available on all platforms, so we polyfill it here for now.
pyCMScopyAux(hTransform, imOut, im); pyCMScopyAux(hTransform, imOut, im);
Py_END_ALLOW_THREADS Py_END_ALLOW_THREADS;
return 0; return 0;
} }
@ -378,7 +378,7 @@ _buildTransform(
) { ) {
cmsHTRANSFORM hTransform; cmsHTRANSFORM hTransform;
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS;
/* create the transform */ /* create the transform */
hTransform = cmsCreateTransform( hTransform = cmsCreateTransform(
@ -412,7 +412,7 @@ _buildProofTransform(
) { ) {
cmsHTRANSFORM hTransform; cmsHTRANSFORM hTransform;
Py_BEGIN_ALLOW_THREADS Py_BEGIN_ALLOW_THREADS;
/* create the transform */ /* create the transform */
hTransform = cmsCreateProofingTransform( hTransform = cmsCreateProofingTransform(

View File

@ -690,9 +690,10 @@ PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) {
SetWindowLongPtr(wnd, 0, (LONG_PTR)callback); SetWindowLongPtr(wnd, 0, (LONG_PTR)callback);
SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get()); 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 */ 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);
} }
@ -701,11 +702,12 @@ PyObject *
PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { PyImaging_EventLoopWin32(PyObject *self, PyObject *args) {
MSG msg; 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); TranslateMessage(&msg);
DispatchMessage(&msg); DispatchMessage(&msg);
} }
Py_END_ALLOW_THREADS Py_END_ALLOW_THREADS;
Py_INCREF(Py_None); Py_INCREF(Py_None);
return Py_None; return Py_None;

View File

@ -736,7 +736,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
} }
if (tag_type) { if (tag_type) {
int type_int = PyLong_AsLong(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; type = (TIFFDataType)type_int;
} }
} }
@ -929,7 +929,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
); );
} else if (type == TIFF_LONG) { } else if (type == TIFF_LONG) {
status = ImagingLibTiffSetField( 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) { } else if (type == TIFF_SSHORT) {
status = ImagingLibTiffSetField( status = ImagingLibTiffSetField(
@ -959,6 +959,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
status = ImagingLibTiffSetField( status = ImagingLibTiffSetField(
&encoder->state, (ttag_t)key_int, (FLOAT64)PyFloat_AsDouble(value) &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 { } else {
TRACE( TRACE(
("Unhandled type for key %d : %s \n", ("Unhandled type for key %d : %s \n",

View File

@ -501,7 +501,8 @@ polygon_generic(
// Needed to draw consistent polygons // Needed to draw consistent polygons
xx[j] = xx[j - 1]; xx[j] = xx[j - 1];
j++; 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 // Connect discontiguous corners
for (k = 0; k < i; k++) { for (k = 0; k < i; k++) {
Edge *other_edge = edge_table[k]; Edge *other_edge = edge_table[k];
@ -510,10 +511,8 @@ polygon_generic(
continue; continue;
} }
// Check if the two edges join to make a corner // Check if the two edges join to make a corner
if (((ymin == current->ymin && ymin == other_edge->ymin) || if (xx[j - 1] ==
(ymin == current->ymax && ymin == other_edge->ymax)) && (ymin - other_edge->y0) * other_edge->dx + other_edge->x0) {
xx[j - 1] == (ymin - other_edge->y0) * other_edge->dx +
other_edge->x0) {
// Determine points from the edges on the next row // Determine points from the edges on the next row
// Or if this is the last row, check the previous row // Or if this is the last row, check the previous row
int offset = ymin == ymax ? -1 : 1; int offset = ymin == ymax ? -1 : 1;

@ -1 +1 @@
Subproject commit 9a9d1275f025f737cdaa3c451ba07129dd95f361 Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155

View File

@ -7,6 +7,7 @@ import re
import shutil import shutil
import struct import struct
import subprocess import subprocess
import sys
from typing import Any from typing import Any
@ -113,26 +114,24 @@ V = {
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16", "FRIBIDI": "1.0.16",
"HARFBUZZ": "10.1.0", "HARFBUZZ": "10.1.0",
"JPEGTURBO": "3.0.4", "JPEGTURBO": "3.1.0",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.44", "LIBPNG": "1.6.44",
"LIBWEBP": "1.4.0", "LIBWEBP": "1.5.0",
"OPENJPEG": "2.5.3", "OPENJPEG": "2.5.3",
"TIFF": "4.6.0", "TIFF": "4.6.0",
"XZ": "5.6.3", "XZ": "5.6.3",
"ZLIB": "1.3.1", "ZLIBNG": "2.2.2",
} }
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "")
# dependencies, listed in order of compilation # dependencies, listed in order of compilation
DEPS: dict[str, dict[str, Any]] = { DEPS: dict[str, dict[str, Any]] = {
"libjpeg": { "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", "filename": f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz",
"dir": f"libjpeg-turbo-{V['JPEGTURBO']}",
"license": ["README.ijg", "LICENSE.md"], "license": ["README.ijg", "LICENSE.md"],
"license_pattern": ( "license_pattern": (
"(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" "(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("cjpeg-static.exe", "cjpeg.exe"),
cmd_copy("djpeg-static.exe", "djpeg.exe"), cmd_copy("djpeg-static.exe", "djpeg.exe"),
], ],
"headers": ["j*.h"], "headers": ["jconfig.h", r"src\j*.h"],
"libs": ["libjpeg.lib"], "libs": ["libjpeg.lib"],
"bins": ["cjpeg.exe", "djpeg.exe"], "bins": ["cjpeg.exe", "djpeg.exe"],
}, },
"zlib": { "zlib": {
"url": "https://zlib.net/FILENAME", "url": f"https://github.com/zlib-ng/zlib-ng/archive/refs/tags/{V['ZLIBNG']}.tar.gz",
"filename": f"zlib{V['ZLIB_DOTLESS']}.zip", "filename": f"zlib-ng-{V['ZLIBNG']}.tar.gz",
"dir": f"zlib-{V['ZLIB']}", "license": "LICENSE.md",
"license": "README", "patch": {
"license_pattern": "Copyright notice:\n\n(.+)$", r"CMakeLists.txt": {
"set_target_properties(zlib PROPERTIES OUTPUT_NAME zlibstatic${{SUFFIX}})": "set_target_properties(zlib PROPERTIES OUTPUT_NAME zlib)", # noqa: E501
},
},
"build": [ "build": [
cmd_nmake(r"win32\Makefile.msc", "clean"), *cmds_cmake(
cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), "zlib", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DZLIB_COMPAT:BOOL=ON"
cmd_copy("zlib.lib", "z.lib"), ),
], ],
"headers": [r"z*.h"], "headers": [r"z*.h"],
"libs": [r"*.lib"], "libs": [r"zlib.lib"],
}, },
"xz": { "xz": {
"url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME", "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/FILENAME",
"filename": f"xz-{V['XZ']}.tar.gz", "filename": f"xz-{V['XZ']}.tar.gz",
"dir": f"xz-{V['XZ']}",
"license": "COPYING", "license": "COPYING",
"build": [ "build": [
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
@ -189,7 +190,6 @@ DEPS: dict[str, dict[str, Any]] = {
"libwebp": { "libwebp": {
"url": "http://downloads.webmproject.org/releases/webp/FILENAME", "url": "http://downloads.webmproject.org/releases/webp/FILENAME",
"filename": f"libwebp-{V['LIBWEBP']}.tar.gz", "filename": f"libwebp-{V['LIBWEBP']}.tar.gz",
"dir": f"libwebp-{V['LIBWEBP']}",
"license": "COPYING", "license": "COPYING",
"patch": { "patch": {
r"src\enc\picture_csp_enc.c": { r"src\enc\picture_csp_enc.c": {
@ -211,7 +211,6 @@ DEPS: dict[str, dict[str, Any]] = {
"libtiff": { "libtiff": {
"url": "https://download.osgeo.org/libtiff/FILENAME", "url": "https://download.osgeo.org/libtiff/FILENAME",
"filename": f"tiff-{V['TIFF']}.tar.gz", "filename": f"tiff-{V['TIFF']}.tar.gz",
"dir": f"tiff-{V['TIFF']}",
"license": "LICENSE.md", "license": "LICENSE.md",
"patch": { "patch": {
r"libtiff\tif_lzma.c": { 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']}/" "url": f"{SF_PROJECTS}/libpng/files/libpng{V['LIBPNG_XY']}/{V['LIBPNG']}/"
f"lpng{V['LIBPNG_DOTLESS']}.zip/download", f"lpng{V['LIBPNG_DOTLESS']}.zip/download",
"filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", "filename": f"lpng{V['LIBPNG_DOTLESS']}.zip",
"dir": f"lpng{V['LIBPNG_DOTLESS']}",
"license": "LICENSE", "license": "LICENSE",
"build": [ "build": [
*cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"),
@ -258,7 +256,6 @@ DEPS: dict[str, dict[str, Any]] = {
"brotli": { "brotli": {
"url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz", "url": f"https://github.com/google/brotli/archive/refs/tags/v{V['BROTLI']}.tar.gz",
"filename": f"brotli-{V['BROTLI']}.tar.gz", "filename": f"brotli-{V['BROTLI']}.tar.gz",
"dir": f"brotli-{V['BROTLI']}",
"license": "LICENSE", "license": "LICENSE",
"build": [ "build": [
*cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"), *cmds_cmake(("brotlicommon", "brotlidec"), "-DBUILD_SHARED_LIBS:BOOL=OFF"),
@ -269,7 +266,6 @@ DEPS: dict[str, dict[str, Any]] = {
"freetype": { "freetype": {
"url": "https://download.savannah.gnu.org/releases/freetype/FILENAME", "url": "https://download.savannah.gnu.org/releases/freetype/FILENAME",
"filename": f"freetype-{V['FREETYPE']}.tar.gz", "filename": f"freetype-{V['FREETYPE']}.tar.gz",
"dir": f"freetype-{V['FREETYPE']}",
"license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
"patch": { "patch": {
r"builds\windows\vc2010\freetype.vcxproj": { r"builds\windows\vc2010\freetype.vcxproj": {
@ -304,7 +300,6 @@ DEPS: dict[str, dict[str, Any]] = {
"lcms2": { "lcms2": {
"url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download", "url": f"{SF_PROJECTS}/lcms/files/lcms/{V['LCMS2']}/FILENAME/download",
"filename": f"lcms2-{V['LCMS2']}.tar.gz", "filename": f"lcms2-{V['LCMS2']}.tar.gz",
"dir": f"lcms2-{V['LCMS2']}",
"license": "LICENSE", "license": "LICENSE",
"patch": { "patch": {
r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
@ -330,7 +325,6 @@ DEPS: dict[str, dict[str, Any]] = {
"openjpeg": { "openjpeg": {
"url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz", "url": f"https://github.com/uclouvain/openjpeg/archive/v{V['OPENJPEG']}.tar.gz",
"filename": f"openjpeg-{V['OPENJPEG']}.tar.gz", "filename": f"openjpeg-{V['OPENJPEG']}.tar.gz",
"dir": f"openjpeg-{V['OPENJPEG']}",
"license": "LICENSE", "license": "LICENSE",
"build": [ "build": [
*cmds_cmake( *cmds_cmake(
@ -345,7 +339,6 @@ DEPS: dict[str, dict[str, Any]] = {
# commit: Merge branch 'master' into msvc (matches 2.17.0 tag) # commit: Merge branch 'master' into msvc (matches 2.17.0 tag)
"url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
"filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
"dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab",
"license": "COPYRIGHT", "license": "COPYRIGHT",
"patch": { "patch": {
"CMakeLists.txt": { "CMakeLists.txt": {
@ -365,7 +358,6 @@ DEPS: dict[str, dict[str, Any]] = {
"harfbuzz": { "harfbuzz": {
"url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip", "url": f"https://github.com/harfbuzz/harfbuzz/archive/{V['HARFBUZZ']}.zip",
"filename": f"harfbuzz-{V['HARFBUZZ']}.zip", "filename": f"harfbuzz-{V['HARFBUZZ']}.zip",
"dir": f"harfbuzz-{V['HARFBUZZ']}",
"license": "COPYING", "license": "COPYING",
"build": [ "build": [
*cmds_cmake( *cmds_cmake(
@ -380,7 +372,6 @@ DEPS: dict[str, dict[str, Any]] = {
"fribidi": { "fribidi": {
"url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip", "url": f"https://github.com/fribidi/fribidi/archive/v{V['FRIBIDI']}.zip",
"filename": f"fribidi-{V['FRIBIDI']}.zip", "filename": f"fribidi-{V['FRIBIDI']}.zip",
"dir": f"fribidi-{V['FRIBIDI']}",
"license": "COPYING", "license": "COPYING",
"build": [ "build": [
cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"), cmd_copy(r"COPYING", rf"{{bin_dir}}\fribidi-{V['FRIBIDI']}-COPYING"),
@ -517,6 +508,9 @@ def extract_dep(url: str, filename: str, prefs: dict[str, str]) -> None:
if sources_dir_abs != member_prefix: if sources_dir_abs != member_prefix:
msg = "Attempted Path Traversal in Tar File" msg = "Attempted Path Traversal in Tar File"
raise RuntimeError(msg) raise RuntimeError(msg)
if sys.version_info >= (3, 12):
tgz.extractall(sources_dir, filter="data")
else:
tgz.extractall(sources_dir) tgz.extractall(sources_dir)
else: else:
msg = "Unknown archive type: " + filename msg = "Unknown archive type: " + filename
@ -760,6 +754,8 @@ def main() -> None:
} }
for k, v in DEPS.items(): 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"]) prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"])
print() print()