Merge branch 'main' into libavif-plugin

This commit is contained in:
Andrew Murray 2025-01-04 10:04:54 +11:00 committed by GitHub
commit be0283061a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 544 additions and 401 deletions

View File

@ -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 nasm
fi

View File

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

View File

@ -44,6 +44,7 @@ jobs:
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
centos-stream-10-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-40-amd64,

View File

@ -37,7 +37,7 @@ fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.1.0
@ -45,13 +45,9 @@ OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
GIFLIB_VERSION=5.2.2
else
GIFLIB_VERSION=5.2.1
fi
ZLIB_NG_VERSION=2.2.2
LIBWEBP_VERSION=1.4.0
GIFLIB_VERSION=5.2.2
ZLIB_NG_VERSION=2.2.3
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
@ -210,6 +206,14 @@ function build {
CFLAGS="$CFLAGS -O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
# For giflib 5.2.2
elif [ -n "$IS_ALPINE" ]; then
apk add imagemagick
else
if [[ "$MB_ML_VER" == "_2_28" ]]; then
yum install -y epel-release
fi
yum install -y ImageMagick
fi
build_libwebp
CFLAGS=$ORIGINAL_CFLAGS

View File

@ -263,8 +263,6 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
cache-dependency-path: "Makefile"
- run: make sdist

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.1
rev: v0.8.4
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@ -24,7 +24,7 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.4
rev: v19.1.5
hooks:
- id: clang-format
types: [c]
@ -56,6 +56,11 @@ repos:
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v0.10.0
hooks:
- id: zizmor
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0
hooks:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

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

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from PIL import Image
from PIL import BlpImagePlugin, Image
from .helper import (
assert_image_equal,
@ -19,6 +19,7 @@ def test_load_blp1() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
assert im.mode == "RGBA"
im.load()
@ -37,6 +38,13 @@ def test_load_blp2_dxt1a() -> None:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
def test_invalid_file() -> None:
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(BlpImagePlugin.BLPFormatError):
BlpImagePlugin.BlpImageFile(invalid_file)
def test_save(tmp_path: Path) -> None:
f = str(tmp_path / "temp.blp")

View File

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

View File

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

View File

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

View File

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

View File

@ -181,6 +181,10 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200)
assert test(0) is None # square pixels
def test_dpi_jfif_cm(self):
with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
assert im.info["dpi"] == (2.54, 5.08)
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@ -349,7 +353,6 @@ class TestFileJpeg:
assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im)
assert transposed is not None
exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {}
@ -1000,8 +1003,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

View File

@ -325,6 +325,18 @@ def test_cmyk() -> None:
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
@skip_unless_feature_version("jpg_2000", "2.5.3")
def test_cmyk_save() -> None:
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2:
assert jp2.mode == "CMYK"
im = roundtrip(jp2)
assert_image_equal(im, jp2)
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im:
@ -424,8 +436,9 @@ def test_pclr() -> None:
def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"):
with Image.open(path) as im:
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
# Test an image that is truncated partway through a codestream
with open("Tests/images/comment.jp2", "rb") as fp:

View File

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

View File

@ -772,22 +772,18 @@ class TestFilePng:
im.seek(1)
@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_PNG_FILE) as im:
im.save(sys.stdout, "PNG")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:

View File

@ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None:
@pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout
def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
monkeypatch.setattr(sys, "stdout", mystdout)
with Image.open(TEST_FILE) as im:
im.save(sys.stdout, "PPM")
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:

View File

@ -115,6 +115,13 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_bigtiff_save(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
hopper().save(outfile, big_tiff=True)
with Image.open(outfile) as im:
assert im.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif")

View File

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

View File

@ -793,6 +793,10 @@ class TestImage:
ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"}
reloaded_exif = Image.Exif()
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"}
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)

View File

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

View File

@ -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)),

View File

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

View File

@ -405,7 +405,6 @@ def test_exif_transpose() -> None:
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im:
assert "exif" not in im.info
@ -417,7 +416,6 @@ def test_exif_transpose() -> None:
# Repeat the operation to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im)
check(base_im)
@ -433,7 +431,6 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif()
@ -446,14 +443,12 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
# Orientation set directly on Image.Exif
im = hopper()
im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()
@ -464,7 +459,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif()

View File

@ -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")

View File

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

View File

@ -175,6 +175,14 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object.
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.1.0
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
Removed features
----------------

View File

@ -572,10 +572,19 @@ JPEG 2000
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
(``.jp2`` or ``.jpx`` files).
.. versionadded:: 8.3.0
Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
subsampled components.
.. versionadded:: 10.4.0
Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later.
.. versionadded:: 11.1.0
Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later.
Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed
JPEG 2000 files (``.jp2`` or ``.jpx`` files).
When loading, if you set the ``mode`` on the image prior to the
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
@ -1199,6 +1208,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.4.0
**big_tiff**
If true, the image will be saved as a BigTIFF.
.. versionadded:: 11.1.0
**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression

View File

@ -27,6 +27,8 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 |
@ -75,7 +77,7 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+============================+==================+==============+
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
| +----------------------------+------------------+ |
| | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+

View File

@ -1,40 +1,38 @@
11.1.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
TODO
^^^^
ExifTags.IFD.Makernote
^^^^^^^^^^^^^^^^^^^^^^
TODO
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
API Changes
===========
TODO
^^^^
Writing XMP bytes to JPEG and MPO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO
Pillow 11.0.0 added writing XMP data to JPEG and MPO images::
im.info["xmp"] = b"test"
im.save("out.jpg")
However, this meant that XMP data was automatically kept from an opened image,
which is inconsistent with the rest of Pillow's behaviour. This functionality
has been removed. To write XMP data, the ``xmp`` argument can still be used for
JPEG files::
im.save("out.jpg", xmp=b"test")
To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now
be used::
second_im.encoderinfo = {"xmp": b"test"}
im.save("out.mpo", save_all=True, append_images=[second_im])
API Additions
=============
@ -49,9 +47,32 @@ zlib library, and what version of zlib-ng is being used::
features.check_feature("zlib_ng") # True or False
features.version_feature("zlib_ng") # "2.2.2" for example, or None
Saving TIFF as BigTIFF
^^^^^^^^^^^^^^^^^^^^^^
TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
im.save("out.tiff", big_tiff=True)
Other Changes
=============
Reading JPEG 2000 comments
^^^^^^^^^^^^^^^^^^^^^^^^^^
When opening a JPEG 2000 image, the comment may now be read into
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.
Saving JPEG 2000 CMYK images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files.
Minimum C version
^^^^^^^^^^^^^^^^^
C99 is now the minimum version of C required to compile Pillow from source.
zlib-ng in wheels
^^^^^^^^^^^^^^^^^

View File

@ -345,7 +345,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")]

View File

@ -259,21 +259,36 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self) -> None:
self.magic = self.fp.read(4)
self.fp.seek(5, os.SEEK_CUR)
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
self.fp.seek(2, os.SEEK_CUR)
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
if not _accept(self.magic):
msg = f"Bad BLP magic {repr(self.magic)}"
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))]
compression = struct.unpack("<i", self.fp.read(4))[0]
if self.magic == b"BLP1":
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
else:
encoding = struct.unpack("<b", self.fp.read(1))[0]
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
self.fp.seek(1, os.SEEK_CUR) # mips
self._size = struct.unpack("<II", self.fp.read(8))
args: tuple[int, int, bool] | tuple[int, int, bool, int]
if self.magic == b"BLP1":
encoding = struct.unpack("<i", self.fp.read(4))[0]
self.fp.seek(4, os.SEEK_CUR) # subtype
args = (compression, encoding, alpha)
offset = 28
else:
args = (compression, encoding, alpha, alpha_encoding)
offset = 20
decoder = self.magic.decode()
self._mode = "RGBA" if alpha else "RGB"
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
class _BLPBaseDecoder(ImageFile.PyDecoder):
@ -281,7 +296,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try:
self._read_blp_header()
self._read_header()
self._load()
except struct.error as e:
msg = "Truncated BLP file"
@ -292,25 +307,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
def _load(self) -> None:
pass
def _read_blp_header(self) -> None:
assert self.fd is not None
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
self.fd.seek(1, os.SEEK_CUR) # mips
self.size = struct.unpack("<II", self._safe_read(8))
if isinstance(self, BLP1Decoder):
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
self.fd.seek(4, os.SEEK_CUR) # subtype
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _read_header(self) -> None:
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length: int) -> bytes:
assert self.fd is not None
@ -326,9 +325,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a))
return ret
def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
def _read_bgra(
self, palette: list[tuple[int, int, int, int]], alpha: bool
) -> bytearray:
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
_data = BytesIO(self._safe_read(self._lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
@ -336,7 +337,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
break
b, g, r, a = palette[offset]
d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth:
if alpha:
d += (a,)
data.extend(d)
return data
@ -344,19 +345,21 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder):
def _load(self) -> None:
if self._blp_compression == Format.JPEG:
self._compression, self._encoding, alpha = self.args
if self._compression == Format.JPEG:
self._decode_jpeg_stream()
elif self._blp_compression == 1:
if self._blp_encoding in (4, 5):
elif self._compression == 1:
if self._encoding in (4, 5):
palette = self._read_palette()
data = self._read_bgra(palette)
data = self._read_bgra(palette, alpha)
self.set_as_raw(data)
else:
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg)
def _decode_jpeg_stream(self) -> None:
@ -365,8 +368,8 @@ class BLP1Decoder(_BLPBaseDecoder):
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0])
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._lengths[0])
data = jpeg_header + data
image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size)
@ -383,47 +386,47 @@ class BLP1Decoder(_BLPBaseDecoder):
class BLP2Decoder(_BLPBaseDecoder):
def _load(self) -> None:
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
palette = self._read_palette()
assert self.fd is not None
self.fd.seek(self._blp_offsets[0])
self.fd.seek(self._offsets[0])
if self._blp_compression == 1:
if self._compression == 1:
# Uncompressed or DirectX compression
if self._blp_encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette)
if self._encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette, alpha)
elif self._blp_encoding == Encoding.DXT:
elif self._encoding == Encoding.DXT:
data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt1(
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
):
if self._alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.state.xsize + 3) // 4 * 8
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt1(self._safe_read(linesize), alpha):
data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
elif self._alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)):
data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
elif self._alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
msg = f"Unknown BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unknown BLP compression {repr(self._blp_compression)}"
msg = f"Unknown BLP compression {repr(self._compression)}"
raise BLPFormatError(msg)
self.set_as_raw(data)
@ -472,10 +475,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
assert im.palette is not None
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
if magic == b"BLP1":
fp.write(struct.pack("<L", alpha_depth))
else:
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", alpha_depth))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1":
fp.write(struct.pack("<i", 5))

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("<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:

View File

@ -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")

View File

@ -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,41 @@ 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
Makernote = 0x927C # Deprecated
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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -1567,7 +1567,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))
@ -1579,11 +1579,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:
@ -2558,7 +2558,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()
@ -2605,6 +2605,11 @@ class Image:
except PermissionError:
pass
raise
finally:
try:
del self.encoderinfo
except AttributeError:
pass
if open_fp:
fp.close()
@ -3886,7 +3891,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::
@ -4020,6 +4025,9 @@ class Exif(_ExifBase):
head = self._get_head()
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, ifd_dict in self._ifds.items():
if tag not in self:
ifd[tag] = ifd_dict
for tag, value in self.items():
if tag in [
ExifTags.IFD.Exif,
@ -4049,11 +4057,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":
@ -4140,7 +4148,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

View File

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

View File

@ -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):

View File

@ -104,28 +104,17 @@ def grab(
def grabclipboard() -> Image.Image | list[str] | None:
if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp(".png")
os.close(fh)
commands = [
'set theFile to (open for access POSIX file "'
+ filepath
+ '" with write permission)',
"try",
" write (the clipboard as «class PNGf») to theFile",
"end try",
"close access theFile",
]
script = ["osascript"]
for command in commands:
script += ["-e", command]
subprocess.call(script)
p = subprocess.run(
["osascript", "-e", "get the clipboard as «class PNGf»"],
capture_output=True,
)
if p.returncode != 0:
return None
im = None
if os.stat(filepath).st_size != 0:
im = Image.open(filepath)
im.load()
os.unlink(filepath)
return im
import binascii
data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3]))
return Image.open(data)
elif sys.platform == "win32":
fmt, data = Image.core.grabclipboard_win32()
if fmt == "file": # CF_HDROP

View File

@ -22,7 +22,7 @@ import functools
import operator
import re
from collections.abc import Sequence
from typing import Protocol, cast
from typing import Literal, Protocol, cast, overload
from . import ExifTags, Image, ImagePalette
@ -673,6 +673,16 @@ def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
return _lut(image, lut)
@overload
def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
@overload
def exif_transpose(
image: Image.Image, *, in_place: Literal[False] = False
) -> Image.Image: ...
def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
"""
If an image has an EXIF Orientation tag, other than 1, transpose the image
@ -698,10 +708,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()

View File

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

View File

@ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
if sig == b"\xff\x4f\xff\x51":
self.codec = "j2k"
self._size, self._mode = _parse_codestream(self.fp)
self._parse_comment()
else:
sig = sig + self.fp.read(8)
@ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
if dpi is not None:
self.info["dpi"] = dpi
if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"):
hdr = self.fp.read(2)
length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR)
self._parse_comment()
else:
msg = "not a JPEG 2000 file"
@ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
]
def _parse_comment(self) -> None:
hdr = self.fp.read(2)
length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR)
while True:
marker = self.fp.read(2)
if not marker:

View File

@ -90,6 +90,9 @@ def APP(self: JpegImageFile, marker: int) -> None:
else:
if jfif_unit == 1:
self.info["dpi"] = jfif_density
elif jfif_unit == 2: # cm
# 1 dpcm = 2.54 dpi
self.info["dpi"] = tuple(d * 2.54 for d in jfif_density)
self.info["jfif_unit"] = jfif_unit
self.info["jfif_density"] = jfif_density
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":
@ -395,6 +398,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 +761,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

View File

@ -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")])
#

View File

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

View File

@ -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)]
#

View File

@ -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):

View File

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

View File

@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __init__(
self,
ifh: bytes = b"II\052\0\0\0\0\0",
ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00",
prefix: bytes | None = None,
group: int | None = None,
) -> None:
@ -949,16 +949,26 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg))
return
def _get_ifh(self):
ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
if self._bigtiff:
ifh += self._pack("HH", 8, 0)
ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
return ifh
def tobytes(self, offset: int = 0) -> bytes:
# FIXME What about tagdata?
result = self._pack("H", len(self._tags_v2))
result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
entries: list[tuple[int, int, int, bytes, bytes]] = []
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
offset += len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + 4
stripoffsets = None
# pass 1: convert tags to binary format
# always write tags in ascending order
fmt = "Q" if self._bigtiff else "L"
fmt_size = 8 if self._bigtiff else 4
for tag, value in sorted(self._tags_v2.items()):
if tag == STRIPOFFSETS:
stripoffsets = len(entries)
@ -966,11 +976,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd:
if self._endian == "<":
ifh = b"II\x2A\x00\x08\x00\x00\x00"
else:
ifh = b"MM\x00\x2A\x00\x00\x00\x08"
ifd = ImageFileDirectory_v2(ifh, group=tag)
ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
values = self._tags_v2[tag]
for ifd_tag, ifd_value in values.items():
ifd[ifd_tag] = ifd_value
@ -993,10 +999,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
else:
count = len(values)
# figure out if data fits into the entry
if len(data) <= 4:
entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
if len(data) <= fmt_size:
entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
else:
entries.append((tag, typ, count, self._pack("L", offset), data))
entries.append((tag, typ, count, self._pack(fmt, offset), data))
offset += (len(data) + 1) // 2 * 2 # pad to word
# update strip offset data to point beyond auxiliary data
@ -1007,13 +1013,15 @@ class ImageFileDirectory_v2(_IFDv2Base):
values = [val + offset for val in handler(self, data, self.legacy_api)]
data = self._write_dispatch[typ](self, *values)
else:
value = self._pack("L", self._unpack("L", value)[0] + offset)
value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
entries[stripoffsets] = tag, typ, count, value, data
# pass 2: write entries to file
for tag, typ, count, value, data in entries:
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
result += self._pack("HHL4s", tag, typ, count, value)
result += self._pack(
"HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
)
# -- overwrite here for multi-page --
result += b"\0\0\0\0" # end of entries
@ -1028,8 +1036,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def save(self, fp: IO[bytes]) -> int:
if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8
fp.write(self._prefix + self._pack("HL", 42, 8))
fp.write(self._get_ifh())
offset = fp.tell()
result = self.tobytes(offset)
@ -1432,8 +1439,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)
@ -1555,17 +1566,6 @@ class TiffImageFile(ImageFile.ImageFile):
# fillorder==2 modes have a corresponding
# fillorder=1 mode
self._mode, rawmode = OPEN_INFO[key]
# libtiff always returns the bytes in native order.
# we're expecting image byte order. So, if the rawmode
# contains I;16, we need to convert from native to image
# byte order.
if rawmode == "I;16":
rawmode = "I;16N"
if ";16B" in rawmode:
rawmode = rawmode.replace(";16B", ";16N")
if ";16L" in rawmode:
rawmode = rawmode.replace(";16L", ";16N")
# YCbCr images with new jpeg compression with pixels in one plane
# unpacked straight into RGB values
if (
@ -1574,6 +1574,14 @@ class TiffImageFile(ImageFile.ImageFile):
and self._planar_configuration == 1
):
rawmode = "RGB"
# libtiff always returns the bytes in native order.
# we're expecting image byte order. So, if the rawmode
# contains I;16, we need to convert from native to image
# byte order.
elif rawmode == "I;16":
rawmode = "I;16N"
elif rawmode.endswith(";16B") or rawmode.endswith(";16L"):
rawmode = rawmode[:-1] + "N"
# Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds
@ -1679,10 +1687,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
msg = f"cannot write mode {im.mode} as TIFF"
raise OSError(msg) from e
ifd = ImageFileDirectory_v2(prefix=prefix)
encoderinfo = im.encoderinfo
encoderconfig = im.encoderconfig
ifd = ImageFileDirectory_v2(prefix=prefix)
if encoderinfo.get("big_tiff"):
ifd._bigtiff = True
try:
compression = encoderinfo["compression"]
except KeyError:
@ -1914,7 +1925,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

View File

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

View File

@ -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)
]

View File

@ -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")

View File

@ -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:
#

View File

@ -1,4 +1,4 @@
# Master version for Pillow
from __future__ import annotations
__version__ = "11.1.0.dev0"
__version__ = "11.2.0.dev0"

View File

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

View File

@ -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;
}

View File

@ -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",

View File

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

View File

@ -44,8 +44,6 @@
defines their own types with the same names, so we need to be able to undef
ours before including the JPEG code. */
#if __STDC_VERSION__ >= 199901L /* C99+ */
#include <stdint.h>
#define INT8 int8_t
@ -55,34 +53,6 @@
#define INT32 int32_t
#define UINT32 uint32_t
#else /* < C99 */
#define INT8 signed char
#if SIZEOF_SHORT == 2
#define INT16 short
#elif SIZEOF_INT == 2
#define INT16 int
#else
#error Cannot find required 16-bit integer type
#endif
#if SIZEOF_SHORT == 4
#define INT32 short
#elif SIZEOF_INT == 4
#define INT32 int
#elif SIZEOF_LONG == 4
#define INT32 long
#else
#error Cannot find required 32-bit integer type
#endif
#define UINT8 unsigned char
#define UINT16 unsigned INT16
#define UINT32 unsigned INT32
#endif /* < C99 */
#endif /* not WIN */
/* assume IEEE; tweak if necessary (patches are welcome) */

View File

@ -640,7 +640,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
opj_dparameters_t params;
OPJ_COLOR_SPACE color_space;
j2k_unpacker_t unpack = NULL;
size_t buffer_size = 0, tile_bytes = 0;
size_t tile_bytes = 0;
unsigned n, tile_height, tile_width;
int subsampling;
int total_component_width = 0;
@ -870,7 +870,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
tile_info.data_size = tile_bytes;
}
if (buffer_size < tile_info.data_size) {
if (tile_info.data_size > 0) {
/* malloc check ok, overflow and tile size sanity check above */
UINT8 *new = realloc(state->buffer, tile_info.data_size);
if (!new) {
@ -883,7 +883,6 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
to valgrind errors. */
memset(new, 0, tile_info.data_size);
state->buffer = new;
buffer_size = tile_info.data_size;
}
if (!opj_decode_tile_data(

View File

@ -330,6 +330,13 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
components = 4;
color_space = OPJ_CLRSPC_SRGB;
pack = j2k_pack_rgba;
#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR == 5 && OPJ_VERSION_BUILD >= 3) || \
(OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR > 5) || OPJ_VERSION_MAJOR > 2)
} else if (strcmp(im->mode, "CMYK") == 0) {
components = 4;
color_space = OPJ_CLRSPC_CMYK;
pack = j2k_pack_rgba;
#endif
} else {
state->errcode = IMAGING_CODEC_BROKEN;
state->state = J2K_STATE_FAILED;

View File

@ -1695,6 +1695,7 @@ static struct {
#ifdef WORDS_BIGENDIAN
{"RGB", "RGB;16N", 48, unpackRGB16B},
{"RGB", "RGBX;16N", 64, unpackRGBA16B},
{"RGBA", "RGBa;16N", 64, unpackRGBa16B},
{"RGBA", "RGBA;16N", 64, unpackRGBA16B},
{"RGBX", "RGBX;16N", 64, unpackRGBA16B},
@ -1708,6 +1709,7 @@ static struct {
{"RGBA", "A;16N", 16, band316B},
#else
{"RGB", "RGB;16N", 48, unpackRGB16L},
{"RGB", "RGBX;16N", 64, unpackRGBA16L},
{"RGBA", "RGBa;16N", 64, unpackRGBa16L},
{"RGBA", "RGBA;16N", 64, unpackRGBA16L},
{"RGBX", "RGBX;16N", 64, unpackRGBA16L},

@ -1 +1 @@
Subproject commit 74a9795bc64ff786b7e7d33bdec2843cf17e512e
Subproject commit 42d761728d141d8462cd9943f4329f12fe62b155

View File

@ -117,11 +117,11 @@ V = {
"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",
"ZLIBNG": "2.2.2",
"ZLIBNG": "2.2.3",
"LIBAVIF": "1.1.1",
}
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
@ -529,7 +529,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)