Merge branch 'main' into context_manager

This commit is contained in:
Andrew Murray 2025-01-09 15:05:47 +11:00
commit 66fe28956b
35 changed files with 425 additions and 259 deletions

View File

@ -2,8 +2,4 @@
# gather the coverage data # gather the coverage data
python3 -m pip install coverage python3 -m pip install coverage
if [[ $MATRIX_DOCKER ]]; then python3 -m coverage xml
python3 -m coverage xml --ignore-errors
else
python3 -m coverage xml
fi

View File

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

View File

@ -52,7 +52,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v4 uses: cygwin/cygwin-install-action@v5
with: with:
packages: > packages: >
gcc-g++ gcc-g++

View File

@ -44,6 +44,7 @@ jobs:
amazon-2023-amd64, amazon-2023-amd64,
arch, arch,
centos-stream-9-amd64, centos-stream-9-amd64,
centos-stream-10-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-40-amd64, fedora-40-amd64,
@ -89,15 +90,15 @@ jobs:
- name: After success - name: After success
run: | run: |
PATH="$PATH:~/.local/bin"
docker start pillow_container docker start pillow_container
sudo docker cp pillow_container:/Pillow /Pillow
sudo chown -R runner /Pillow
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'` pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
docker stop pillow_container docker stop pillow_container
sudo mkdir -p $pil_path sudo mkdir -p $pil_path
sudo cp src/PIL/*.py $pil_path sudo cp src/PIL/*.py $pil_path
cd /Pillow
.ci/after_success.sh .ci/after_success.sh
env:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5

View File

@ -37,20 +37,15 @@ fi
ARCHIVE_SDIR=pillow-depends-main ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds # Package versions for fresh source builds
FREETYPE_VERSION=2.13.2 FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=10.1.0 HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44 LIBPNG_VERSION=1.6.45
JPEGTURBO_VERSION=3.1.0 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
LCMS2_VERSION=2.16 LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then ZLIB_NG_VERSION=2.2.3
GIFLIB_VERSION=5.2.2
else
GIFLIB_VERSION=5.2.1
fi
ZLIB_NG_VERSION=2.2.2
LIBWEBP_VERSION=1.5.0 LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8 BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0 LIBXCB_VERSION=1.17.0
@ -103,7 +98,7 @@ function build_harfbuzz {
function build { function build {
build_xz build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel yum remove -y zlib-devel
fi fi
build_zlib_ng build_zlib_ng
@ -140,7 +135,9 @@ function build {
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
fi fi
build_libwebp build_simple libwebp $LIBWEBP_VERSION \
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
--enable-libwebpmux --enable-libwebpdemux
CFLAGS=$ORIGINAL_CFLAGS CFLAGS=$ORIGINAL_CFLAGS
build_brotli build_brotli

View File

@ -13,6 +13,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py" - "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
@ -23,6 +24,7 @@ on:
paths: paths:
- ".ci/requirements-cibw.txt" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*" - ".github/workflows/wheel*"
- "pyproject.toml"
- "setup.py" - "setup.py"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py" - "winbuild/build_prepare.py"
@ -263,8 +265,6 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip
cache-dependency-path: "Makefile"
- run: make sdist - run: make sdist

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

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

View File

@ -186,6 +186,10 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200) assert test(100, 200) == (100, 200)
assert test(0) is None # square pixels assert test(0) is None # square pixels
def test_dpi_jfif_cm(self) -> None:
with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
assert im.info["dpi"] == (2.54, 5.08)
@mark_if_feature_version( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
@ -282,7 +286,10 @@ class TestFileJpeg:
assert not im2.info.get("progressive") assert not im2.info.get("progressive")
assert im3.info.get("progressive") assert im3.info.get("progressive")
assert_image_equal(im1, im3) if features.check_feature("mozjpeg"):
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im3)
assert im1_bytes >= im3_bytes assert im1_bytes >= im3_bytes
def test_progressive_large_buffer(self, tmp_path: Path) -> None: def test_progressive_large_buffer(self, tmp_path: Path) -> None:
@ -360,7 +367,6 @@ class TestFileJpeg:
assert exif.get_ifd(0x8825) == {} assert exif.get_ifd(0x8825) == {}
transposed = ImageOps.exif_transpose(im) transposed = ImageOps.exif_transpose(im)
assert transposed is not None
exif = transposed.getexif() exif = transposed.getexif()
assert exif.get_ifd(0x8825) == {} assert exif.get_ifd(0x8825) == {}
@ -437,8 +443,12 @@ class TestFileJpeg:
im2 = self.roundtrip(hopper(), progressive=1) im2 = self.roundtrip(hopper(), progressive=1)
im3 = self.roundtrip(hopper(), progression=1) # compatibility im3 = self.roundtrip(hopper(), progression=1) # compatibility
assert_image_equal(im1, im2) if features.check_feature("mozjpeg"):
assert_image_equal(im1, im3) assert_image_similar(im1, im2, 9.39)
assert_image_similar(im1, im3, 9.39)
else:
assert_image_equal(im1, im2)
assert_image_equal(im1, im3)
assert im2.info.get("progressive") assert im2.info.get("progressive")
assert im2.info.get("progression") assert im2.info.get("progression")
assert im3.info.get("progressive") assert im3.info.get("progressive")

View File

@ -328,6 +328,18 @@ def test_cmyk() -> None:
assert im.getpixel((0, 0)) == (185, 134, 0, 0) 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")) @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext: str) -> None: def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im: with Image.open("Tests/images/16bit.cropped" + ext) as im:

View File

@ -1177,7 +1177,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.load() im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY # Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9" assert str(e.value) == "decoder error -9"
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:

View File

@ -7,7 +7,7 @@ from pathlib import Path
import pytest import pytest
from PIL import Image, ImageSequence, SpiderImagePlugin from PIL import Image, SpiderImagePlugin
from .helper import assert_image_equal, hopper, is_pypy from .helper import assert_image_equal, hopper, is_pypy
@ -154,8 +154,8 @@ def test_nonstack_file() -> None:
def test_nonstack_dos() -> None: def test_nonstack_dos() -> None:
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
for i, frame in enumerate(ImageSequence.Iterator(im)): with pytest.raises(EOFError):
assert i <= 1, "Non-stack DOS file test failed" im.seek(0)
# for issue #4093 # for issue #4093

View File

@ -123,6 +123,19 @@ class TestFileTiff:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) 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")
im = hopper()
im.save(outfile, big_tiff=True)
with Image.open(outfile) as reloaded:
assert reloaded.tag_v2._bigtiff is True
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
with Image.open(outfile) as reloaded:
assert reloaded.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None: def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"): with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif") Image.open("Tests/images/seek_too_large.tif")
@ -768,7 +781,7 @@ class TestFileTiff:
assert reread.n_frames == 3 assert reread.n_frames == 3
def test_fixoffsets(self) -> None: def test_fixoffsets(self) -> None:
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a: with TiffImagePlugin.AppendingTiffWriter(b) as a:
b.seek(0) b.seek(0)
a.fixOffsets(1, isShort=True) a.fixOffsets(1, isShort=True)
@ -781,6 +794,37 @@ class TestFileTiff:
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
a.fixOffsets(1) a.fixOffsets(1)
b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**16
b.seek(0)
a.fixOffsets(1, isShort=True)
b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.offsetOfNewPage = 2**32
b.seek(0)
a.fixOffsets(1, isShort=True)
b.seek(0)
a.fixOffsets(1, isLong=True)
def test_appending_tiff_writer_writelong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.writeLong(2**32 - 1)
assert b.getvalue() == data + b"\xff\xff\xff\xff"
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b = BytesIO(data)
with TiffImagePlugin.AppendingTiffWriter(b) as a:
a.rewriteLastShortToLong(2**32 - 1)
assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff"
def test_saving_icc_profile(self, tmp_path: Path) -> None: def test_saving_icc_profile(self, tmp_path: Path) -> None:
# Tests saving TIFF with icc_profile set. # Tests saving TIFF with icc_profile set.
# At the time of writing this will only work for non-compressed tiffs # At the time of writing this will only work for non-compressed tiffs

View File

@ -795,6 +795,10 @@ class TestImage:
ifd[36864] = b"0220" ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {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( @mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )

View File

@ -405,7 +405,6 @@ def test_exif_transpose() -> None:
else: else:
original_exif = im.info["exif"] original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert_image_similar(base_im, transposed_im, 17) assert_image_similar(base_im, transposed_im, 17)
if orientation_im is base_im: if orientation_im is base_im:
assert "exif" not in im.info 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 # Repeat the operation to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im) transposed_im2 = ImageOps.exif_transpose(transposed_im)
assert transposed_im2 is not None
assert_image_equal(transposed_im2, transposed_im) assert_image_equal(transposed_im2, transposed_im)
check(base_im) check(base_im)
@ -434,7 +432,6 @@ def test_exif_transpose() -> None:
assert im.getexif()[0x0112] == 3 assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif() transposed_im._reload_exif()
@ -454,7 +451,6 @@ def test_exif_transpose() -> None:
im = hopper() im = hopper()
im.getexif()[0x0112] = 3 im.getexif()[0x0112] = 3
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
@ -465,7 +461,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
del im.info["xmp"] del im.info["xmp"]
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert transposed_im is not None
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()

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 raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
``Image.Image.getim()``, which returns a ``Capsule`` object. ``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 Removed features
---------------- ----------------

View File

@ -572,10 +572,19 @@ JPEG 2000
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to ``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. ``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 .. versionadded:: 8.3.0
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
(``.jp2`` or ``.jpx`` files). 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 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 :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 .. versionadded:: 8.4.0
**big_tiff**
If true, the image will be saved as a BigTIFF.
.. versionadded:: 11.1.0
**compression** **compression**
A string containing the desired compression method for the A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression 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 9 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS Stream 10 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 | | Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | 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 | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | 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 | | | | 3.8 | 10.4.0 | |
+----------------------------------+----------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+

View File

@ -1,40 +1,38 @@
11.1.0 11.1.0
------ ------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations Deprecations
============ ============
TODO ExifTags.IFD.Makernote
^^^^ ^^^^^^^^^^^^^^^^^^^^^^
TODO ``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
``ExifTags.IFD.MakerNote``.
API Changes 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 API Additions
============= =============
@ -49,6 +47,13 @@ zlib library, and what version of zlib-ng is being used::
features.check_feature("zlib_ng") # True or False features.check_feature("zlib_ng") # True or False
features.version_feature("zlib_ng") # "2.2.2" for example, or None 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 Other Changes
============= =============
@ -58,6 +63,16 @@ Reading JPEG 2000 comments
When opening a JPEG 2000 image, the comment may now be read into 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. :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 zlib-ng in wheels
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^

View File

@ -260,21 +260,36 @@ class BlpImageFile(ImageFile.ImageFile):
def _open(self) -> None: def _open(self) -> None:
assert self.fp is not None assert self.fp is not None
self.magic = self.fp.read(4) self.magic = self.fp.read(4)
if not _accept(self.magic):
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:
msg = f"Bad BLP magic {repr(self.magic)}" msg = f"Bad BLP magic {repr(self.magic)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
self._mode = "RGBA" if self._blp_alpha_depth else "RGB" compression = struct.unpack("<i", self.fp.read(4))[0]
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)] 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): class _BLPBaseDecoder(ImageFile.PyDecoder):
@ -282,7 +297,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try: try:
self._read_blp_header() self._read_header()
self._load() self._load()
except struct.error as e: except struct.error as e:
msg = "Truncated BLP file" msg = "Truncated BLP file"
@ -293,25 +308,9 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
def _load(self) -> None: def _load(self) -> None:
pass pass
def _read_blp_header(self) -> None: def _read_header(self) -> None:
assert self.fd is not None self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self.fd.seek(4) self._lengths = struct.unpack("<16I", self._safe_read(16 * 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 _safe_read(self, length: int) -> bytes: def _safe_read(self, length: int) -> bytes:
assert self.fd is not None assert self.fd is not None
@ -327,9 +326,11 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a)) ret.append((b, g, r, a))
return ret 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 = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0])) _data = BytesIO(self._safe_read(self._lengths[0]))
while True: while True:
try: try:
(offset,) = struct.unpack("<B", _data.read(1)) (offset,) = struct.unpack("<B", _data.read(1))
@ -337,7 +338,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
break break
b, g, r, a = palette[offset] b, g, r, a = palette[offset]
d: tuple[int, ...] = (r, g, b) d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth: if alpha:
d += (a,) d += (a,)
data.extend(d) data.extend(d)
return data return data
@ -345,19 +346,21 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder): class BLP1Decoder(_BLPBaseDecoder):
def _load(self) -> None: 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() self._decode_jpeg_stream()
elif self._blp_compression == 1: elif self._compression == 1:
if self._blp_encoding in (4, 5): if self._encoding in (4, 5):
palette = self._read_palette() palette = self._read_palette()
data = self._read_bgra(palette) data = self._read_bgra(palette, alpha)
self.set_as_raw(data) self.set_as_raw(data)
else: else:
msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" msg = f"Unsupported BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
else: else:
msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
def _decode_jpeg_stream(self) -> None: def _decode_jpeg_stream(self) -> None:
@ -366,65 +369,61 @@ class BLP1Decoder(_BLPBaseDecoder):
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4)) (jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size) jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None assert self.fd is not None
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this? self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0]) data = self._safe_read(self._lengths[0])
data = jpeg_header + data data = jpeg_header + data
image = JpegImageFile(BytesIO(data)) image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size) Image._decompression_bomb_check(image.size)
if image.mode == "CMYK": if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0] args = image.tile[0].args
assert isinstance(args, tuple) assert isinstance(args, tuple)
image.tile = [ image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK")) self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
]
r, g, b = image.convert("RGB").split()
reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes())
class BLP2Decoder(_BLPBaseDecoder): class BLP2Decoder(_BLPBaseDecoder):
def _load(self) -> None: def _load(self) -> None:
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
palette = self._read_palette() palette = self._read_palette()
assert self.fd is not None 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 # Uncompressed or DirectX compression
if self._blp_encoding == Encoding.UNCOMPRESSED: if self._encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette) data = self._read_bgra(palette, alpha)
elif self._blp_encoding == Encoding.DXT: elif self._encoding == Encoding.DXT:
data = bytearray() data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1: if self._alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8 linesize = (self.state.xsize + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4): for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt1( for d in decode_dxt1(self._safe_read(linesize), alpha):
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
):
data += d data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT3: elif self._alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.size[0] + 3) // 4 * 16 linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4): for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)): for d in decode_dxt3(self._safe_read(linesize)):
data += d data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT5: elif self._alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.size[0] + 3) // 4 * 16 linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4): for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)): for d in decode_dxt5(self._safe_read(linesize)):
data += d data += d
else: else:
msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
else: else:
msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" msg = f"Unknown BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
else: else:
msg = f"Unknown BLP compression {repr(self._blp_compression)}" msg = f"Unknown BLP compression {repr(self._compression)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
self.set_as_raw(data) self.set_as_raw(data)
@ -473,10 +472,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
assert im.palette is not None assert im.palette is not None
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression 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)) alpha_depth = 1 if im.palette.mode == "RGBA" else 0
fp.write(struct.pack("<b", 0)) # alpha encoding if magic == b"BLP1":
fp.write(struct.pack("<b", 0)) # mips 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)) fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1": if magic == b"BLP1":
fp.write(struct.pack("<i", 5)) fp.write(struct.pack("<i", 5))

View File

@ -353,6 +353,7 @@ class IFD(IntEnum):
Exif = 0x8769 Exif = 0x8769
GPSInfo = 0x8825 GPSInfo = 0x8825
MakerNote = 0x927C MakerNote = 0x927C
Makernote = 0x927C # Deprecated
Interop = 0xA005 Interop = 0xA005
IFD1 = -1 IFD1 = -1

View File

@ -3963,6 +3963,9 @@ class Exif(_ExifBase):
head = self._get_head() head = self._get_head()
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=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(): for tag, value in self.items():
if tag in [ if tag in [
ExifTags.IFD.Exif, ExifTags.IFD.Exif,

View File

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

View File

@ -92,6 +92,9 @@ def APP(self: JpegImageFile, marker: int) -> None:
else: else:
if jfif_unit == 1: if jfif_unit == 1:
self.info["dpi"] = jfif_density 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_unit"] = jfif_unit
self.info["jfif_density"] = jfif_density self.info["jfif_density"] = jfif_density
elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": elif marker == 0xFFE1 and s[:6] == b"Exif\0\0":

View File

@ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __init__( def __init__(
self, 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, prefix: bytes | None = None,
group: int | None = None, group: int | None = None,
) -> None: ) -> None:
@ -949,12 +949,25 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg)) warnings.warn(str(msg))
return return
def _get_ifh(self) -> bytes:
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: def tobytes(self, offset: int = 0) -> bytes:
# FIXME What about tagdata? # FIXME What about tagdata?
result = self._pack("H", len(self._tags_v2)) result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
entries: list[tuple[int, int, int, bytes, bytes]] = [] entries: list[tuple[int, int, int, bytes, bytes]] = []
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
fmt = "Q" if self._bigtiff else "L"
fmt_size = 8 if self._bigtiff else 4
offset += (
len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
)
stripoffsets = None stripoffsets = None
# pass 1: convert tags to binary format # pass 1: convert tags to binary format
@ -966,11 +979,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict) is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd: if is_ifd:
if self._endian == "<": ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
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)
values = self._tags_v2[tag] values = self._tags_v2[tag]
for ifd_tag, ifd_value in values.items(): for ifd_tag, ifd_value in values.items():
ifd[ifd_tag] = ifd_value ifd[ifd_tag] = ifd_value
@ -993,10 +1002,10 @@ class ImageFileDirectory_v2(_IFDv2Base):
else: else:
count = len(values) count = len(values)
# figure out if data fits into the entry # figure out if data fits into the entry
if len(data) <= 4: if len(data) <= fmt_size:
entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
else: 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 offset += (len(data) + 1) // 2 * 2 # pad to word
# update strip offset data to point beyond auxiliary data # update strip offset data to point beyond auxiliary data
@ -1007,16 +1016,18 @@ class ImageFileDirectory_v2(_IFDv2Base):
values = [val + offset for val in handler(self, data, self.legacy_api)] values = [val + offset for val in handler(self, data, self.legacy_api)]
data = self._write_dispatch[typ](self, *values) data = self._write_dispatch[typ](self, *values)
else: 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 entries[stripoffsets] = tag, typ, count, value, data
# pass 2: write entries to file # pass 2: write entries to file
for tag, typ, count, value, data in entries: for tag, typ, count, value, data in entries:
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) 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 -- # -- overwrite here for multi-page --
result += b"\0\0\0\0" # end of entries result += self._pack(fmt, 0) # end of entries
# pass 3: write auxiliary data to file # pass 3: write auxiliary data to file
for tag, typ, count, value, data in entries: for tag, typ, count, value, data in entries:
@ -1028,8 +1039,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def save(self, fp: IO[bytes]) -> int: def save(self, fp: IO[bytes]) -> int:
if fp.tell() == 0: # skip TIFF header on subsequent pages if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._get_ifh())
fp.write(self._prefix + self._pack("HL", 42, 8))
offset = fp.tell() offset = fp.tell()
result = self.tobytes(offset) result = self.tobytes(offset)
@ -1403,7 +1413,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp = None # might be shared self.fp = None # might be shared
if err < 0: if err < 0:
raise OSError(err) msg = f"decoder error {err}"
raise OSError(msg)
return Image.Image.load(self) return Image.Image.load(self)
@ -1563,17 +1574,6 @@ class TiffImageFile(ImageFile.ImageFile):
# fillorder==2 modes have a corresponding # fillorder==2 modes have a corresponding
# fillorder=1 mode # fillorder=1 mode
self._mode, rawmode = OPEN_INFO[key] 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 # YCbCr images with new jpeg compression with pixels in one plane
# unpacked straight into RGB values # unpacked straight into RGB values
if ( if (
@ -1582,6 +1582,14 @@ class TiffImageFile(ImageFile.ImageFile):
and self._planar_configuration == 1 and self._planar_configuration == 1
): ):
rawmode = "RGB" 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 # Offset in the tile tuple is 0, we go from 0,0 to
# w,h, and we only do this once -- eds # w,h, and we only do this once -- eds
@ -1687,10 +1695,13 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
msg = f"cannot write mode {im.mode} as TIFF" msg = f"cannot write mode {im.mode} as TIFF"
raise OSError(msg) from e raise OSError(msg) from e
ifd = ImageFileDirectory_v2(prefix=prefix)
encoderinfo = im.encoderinfo encoderinfo = im.encoderinfo
encoderconfig = im.encoderconfig encoderconfig = im.encoderconfig
ifd = ImageFileDirectory_v2(prefix=prefix)
if encoderinfo.get("big_tiff"):
ifd._bigtiff = True
try: try:
compression = encoderinfo["compression"] compression = encoderinfo["compression"]
except KeyError: except KeyError:
@ -2040,20 +2051,21 @@ class AppendingTiffWriter(io.BytesIO):
self.offsetOfNewPage = 0 self.offsetOfNewPage = 0
self.IIMM = iimm = self.f.read(4) self.IIMM = iimm = self.f.read(4)
self._bigtiff = b"\x2B" in iimm
if not iimm: if not iimm:
# empty file - first page # empty file - first page
self.isFirst = True self.isFirst = True
return return
self.isFirst = False self.isFirst = False
if iimm == b"II\x2a\x00": if iimm not in PREFIXES:
self.setEndian("<")
elif iimm == b"MM\x00\x2a":
self.setEndian(">")
else:
msg = "Invalid TIFF file header" msg = "Invalid TIFF file header"
raise RuntimeError(msg) raise RuntimeError(msg)
self.setEndian("<" if iimm.startswith(II) else ">")
if self._bigtiff:
self.f.seek(4, os.SEEK_CUR)
self.skipIFDs() self.skipIFDs()
self.goToEnd() self.goToEnd()
@ -2073,11 +2085,13 @@ class AppendingTiffWriter(io.BytesIO):
msg = "IIMM of new page doesn't match IIMM of first page" msg = "IIMM of new page doesn't match IIMM of first page"
raise RuntimeError(msg) raise RuntimeError(msg)
ifd_offset = self.readLong() if self._bigtiff:
self.f.seek(4, os.SEEK_CUR)
ifd_offset = self._read(8 if self._bigtiff else 4)
ifd_offset += self.offsetOfNewPage ifd_offset += self.offsetOfNewPage
assert self.whereToWriteNewIFDOffset is not None assert self.whereToWriteNewIFDOffset is not None
self.f.seek(self.whereToWriteNewIFDOffset) self.f.seek(self.whereToWriteNewIFDOffset)
self.writeLong(ifd_offset) self._write(ifd_offset, 8 if self._bigtiff else 4)
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
self.fixIFD() self.fixIFD()
@ -2123,18 +2137,20 @@ class AppendingTiffWriter(io.BytesIO):
self.endian = endian self.endian = endian
self.longFmt = f"{self.endian}L" self.longFmt = f"{self.endian}L"
self.shortFmt = f"{self.endian}H" self.shortFmt = f"{self.endian}H"
self.tagFormat = f"{self.endian}HHL" self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
def skipIFDs(self) -> None: def skipIFDs(self) -> None:
while True: while True:
ifd_offset = self.readLong() ifd_offset = self._read(8 if self._bigtiff else 4)
if ifd_offset == 0: if ifd_offset == 0:
self.whereToWriteNewIFDOffset = self.f.tell() - 4 self.whereToWriteNewIFDOffset = self.f.tell() - (
8 if self._bigtiff else 4
)
break break
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
num_tags = self.readShort() num_tags = self._read(8 if self._bigtiff else 2)
self.f.seek(num_tags * 12, os.SEEK_CUR) self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
def write(self, data: Buffer, /) -> int: def write(self, data: Buffer, /) -> int:
return self.f.write(data) return self.f.write(data)
@ -2164,17 +2180,19 @@ class AppendingTiffWriter(io.BytesIO):
msg = f"wrote only {bytes_written} bytes but wanted {expected}" msg = f"wrote only {bytes_written} bytes but wanted {expected}"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastShortToLong(self, value: int) -> None: def _rewriteLast(
self.f.seek(-2, os.SEEK_CUR) self, value: int, field_size: int, new_field_size: int = 0
bytes_written = self.f.write(struct.pack(self.longFmt, value)) ) -> None:
self._verify_bytes_written(bytes_written, 4)
def _rewriteLast(self, value: int, field_size: int) -> None:
self.f.seek(-field_size, os.SEEK_CUR) self.f.seek(-field_size, os.SEEK_CUR)
if not new_field_size:
new_field_size = field_size
bytes_written = self.f.write( bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value) struct.pack(self.endian + self._fmt(new_field_size), value)
) )
self._verify_bytes_written(bytes_written, field_size) self._verify_bytes_written(bytes_written, new_field_size)
def rewriteLastShortToLong(self, value: int) -> None:
self._rewriteLast(value, 2, 4)
def rewriteLastShort(self, value: int) -> None: def rewriteLastShort(self, value: int) -> None:
return self._rewriteLast(value, 2) return self._rewriteLast(value, 2)
@ -2182,13 +2200,17 @@ class AppendingTiffWriter(io.BytesIO):
def rewriteLastLong(self, value: int) -> None: def rewriteLastLong(self, value: int) -> None:
return self._rewriteLast(value, 4) return self._rewriteLast(value, 4)
def _write(self, value: int, field_size: int) -> None:
bytes_written = self.f.write(
struct.pack(self.endian + self._fmt(field_size), value)
)
self._verify_bytes_written(bytes_written, field_size)
def writeShort(self, value: int) -> None: def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) self._write(value, 2)
self._verify_bytes_written(bytes_written, 2)
def writeLong(self, value: int) -> None: def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value)) self._write(value, 4)
self._verify_bytes_written(bytes_written, 4)
def close(self) -> None: def close(self) -> None:
self.finalize() self.finalize()
@ -2196,24 +2218,27 @@ class AppendingTiffWriter(io.BytesIO):
self.f.close() self.f.close()
def fixIFD(self) -> None: def fixIFD(self) -> None:
num_tags = self.readShort() num_tags = self._read(8 if self._bigtiff else 2)
for i in range(num_tags): for i in range(num_tags):
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) tag, field_type, count = struct.unpack(
self.tagFormat, self.f.read(12 if self._bigtiff else 8)
)
field_size = self.fieldSizes[field_type] field_size = self.fieldSizes[field_type]
total_size = field_size * count total_size = field_size * count
is_local = total_size <= 4 fmt_size = 8 if self._bigtiff else 4
is_local = total_size <= fmt_size
if not is_local: if not is_local:
offset = self.readLong() + self.offsetOfNewPage offset = self._read(fmt_size) + self.offsetOfNewPage
self.rewriteLastLong(offset) self._rewriteLast(offset, fmt_size)
if tag in self.Tags: if tag in self.Tags:
cur_pos = self.f.tell() cur_pos = self.f.tell()
if is_local: if is_local:
self._fixOffsets(count, field_size) self._fixOffsets(count, field_size)
self.f.seek(cur_pos + 4) self.f.seek(cur_pos + fmt_size)
else: else:
self.f.seek(offset) self.f.seek(offset)
self._fixOffsets(count, field_size) self._fixOffsets(count, field_size)
@ -2221,24 +2246,33 @@ class AppendingTiffWriter(io.BytesIO):
elif is_local: elif is_local:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(fmt_size, os.SEEK_CUR)
def _fixOffsets(self, count: int, field_size: int) -> None: def _fixOffsets(self, count: int, field_size: int) -> None:
for i in range(count): for i in range(count):
offset = self._read(field_size) offset = self._read(field_size)
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
if field_size == 2 and offset >= 65536:
# offset is now too large - we must convert shorts to longs new_field_size = 0
if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
# offset is now too large - we must convert long to long8
new_field_size = 8
elif field_size == 2 and offset >= 2**16:
# offset is now too large - we must convert short to long
new_field_size = 4
if new_field_size:
if count != 1: if count != 1:
msg = "not implemented" msg = "not implemented"
raise RuntimeError(msg) # XXX TODO raise RuntimeError(msg) # XXX TODO
# simple case - the offset is just one and therefore it is # simple case - the offset is just one and therefore it is
# local (not referenced with another offset) # local (not referenced with another offset)
self.rewriteLastShortToLong(offset) self._rewriteLast(offset, field_size, new_field_size)
self.f.seek(-10, os.SEEK_CUR) # Move back past the new offset, past 'count', and before 'field_type'
self.writeShort(TiffTags.LONG) # rewrite the type to LONG rewind = -new_field_size - 4 - 2
self.f.seek(8, os.SEEK_CUR) self.f.seek(rewind, os.SEEK_CUR)
self.writeShort(new_field_size) # rewrite the type
self.f.seek(2 - rewind, os.SEEK_CUR)
else: else:
self._rewriteLast(offset, field_size) self._rewriteLast(offset, field_size)

View File

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

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"),
"mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"),
"zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_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),
@ -300,7 +301,8 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
if name == "jpg": if name == "jpg":
libjpeg_turbo_version = version_feature("libjpeg_turbo") libjpeg_turbo_version = version_feature("libjpeg_turbo")
if libjpeg_turbo_version is not None: if libjpeg_turbo_version is not None:
v = "libjpeg-turbo " + libjpeg_turbo_version v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo"
v += " " + libjpeg_turbo_version
if v is None: if v is None:
v = version(name) v = version(name)
if v is not None: if v is not None:

View File

@ -76,6 +76,13 @@
#ifdef HAVE_LIBJPEG #ifdef HAVE_LIBJPEG
#include "jconfig.h" #include "jconfig.h"
#ifdef LIBJPEG_TURBO_VERSION
#define JCONFIG_INCLUDED
#ifdef __CYGWIN__
#define _BASETSD_H
#endif
#include "jpeglib.h"
#endif
#endif #endif
#ifdef HAVE_LIBZ #ifdef HAVE_LIBZ
@ -4367,6 +4374,15 @@ setup_module(PyObject *m) {
Py_INCREF(have_libjpegturbo); Py_INCREF(have_libjpegturbo);
PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo); PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", have_libjpegturbo);
PyObject *have_mozjpeg;
#ifdef JPEG_C_PARAM_SUPPORTED
have_mozjpeg = Py_True;
#else
have_mozjpeg = Py_False;
#endif
Py_INCREF(have_mozjpeg);
PyModule_AddObject(m, "HAVE_MOZJPEG", have_mozjpeg);
PyObject *have_libimagequant; PyObject *have_libimagequant;
#ifdef HAVE_LIBIMAGEQUANT #ifdef HAVE_LIBIMAGEQUANT
have_libimagequant = Py_True; have_libimagequant = Py_True;

View File

@ -339,29 +339,23 @@ text_layout_raqm(
len = PySequence_Fast_GET_SIZE(seq); len = PySequence_Fast_GET_SIZE(seq);
for (j = 0; j < len; j++) { for (j = 0; j < len; j++) {
PyObject *item = PySequence_Fast_GET_ITEM(seq, j); PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
char *feature = NULL;
Py_ssize_t size = 0;
PyObject *bytes;
if (!PyUnicode_Check(item)) { if (!PyUnicode_Check(item)) {
Py_DECREF(seq); Py_DECREF(seq);
PyErr_SetString(PyExc_TypeError, "expected a string"); PyErr_SetString(PyExc_TypeError, "expected a string");
goto failed; goto failed;
} }
bytes = PyUnicode_AsUTF8String(item);
if (bytes == NULL) { Py_ssize_t size;
const char *feature = PyUnicode_AsUTF8AndSize(item, &size);
if (feature == NULL) {
Py_DECREF(seq); Py_DECREF(seq);
goto failed; goto failed;
} }
feature = PyBytes_AS_STRING(bytes);
size = PyBytes_GET_SIZE(bytes);
if (!raqm_add_font_feature(rq, feature, size)) { if (!raqm_add_font_feature(rq, feature, size)) {
Py_DECREF(seq); Py_DECREF(seq);
Py_DECREF(bytes);
PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed");
goto failed; goto failed;
} }
Py_DECREF(bytes);
} }
Py_DECREF(seq); Py_DECREF(seq);
} }

View File

@ -44,8 +44,6 @@
defines their own types with the same names, so we need to be able to undef defines their own types with the same names, so we need to be able to undef
ours before including the JPEG code. */ ours before including the JPEG code. */
#if __STDC_VERSION__ >= 199901L /* C99+ */
#include <stdint.h> #include <stdint.h>
#define INT8 int8_t #define INT8 int8_t
@ -55,34 +53,6 @@
#define INT32 int32_t #define INT32 int32_t
#define UINT32 uint32_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 */ #endif /* not WIN */
/* assume IEEE; tweak if necessary (patches are welcome) */ /* assume IEEE; tweak if necessary (patches are welcome) */

View File

@ -330,6 +330,13 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
components = 4; components = 4;
color_space = OPJ_CLRSPC_SRGB; color_space = OPJ_CLRSPC_SRGB;
pack = j2k_pack_rgba; 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 { } else {
state->errcode = IMAGING_CODEC_BROKEN; state->errcode = IMAGING_CODEC_BROKEN;
state->state = J2K_STATE_FAILED; state->state = J2K_STATE_FAILED;

View File

@ -134,7 +134,16 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
return -1; return -1;
} }
/* Compressor configuration */ /* Compressor configuration */
#ifdef JPEG_C_PARAM_SUPPORTED
/* MozJPEG */
if (!context->progressive) {
/* Do not use MozJPEG progressive default */
jpeg_c_set_int_param(
&context->cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST
);
}
#endif
jpeg_set_defaults(&context->cinfo); jpeg_set_defaults(&context->cinfo);
/* Prevent RGB -> YCbCr conversion */ /* Prevent RGB -> YCbCr conversion */

View File

@ -1664,6 +1664,7 @@ static struct {
{"RGBA", "RGBaXX", 48, unpackRGBaskip2}, {"RGBA", "RGBaXX", 48, unpackRGBaskip2},
{"RGBA", "RGBa;16L", 64, unpackRGBa16L}, {"RGBA", "RGBa;16L", 64, unpackRGBa16L},
{"RGBA", "RGBa;16B", 64, unpackRGBa16B}, {"RGBA", "RGBa;16B", 64, unpackRGBa16B},
{"RGBA", "BGR", 24, ImagingUnpackBGR},
{"RGBA", "BGRa", 32, unpackBGRa}, {"RGBA", "BGRa", 32, unpackBGRa},
{"RGBA", "RGBA;I", 32, unpackRGBAI}, {"RGBA", "RGBA;I", 32, unpackRGBAI},
{"RGBA", "RGBA;L", 32, unpackRGBAL}, {"RGBA", "RGBA;L", 32, unpackRGBAL},
@ -1695,6 +1696,7 @@ static struct {
#ifdef WORDS_BIGENDIAN #ifdef WORDS_BIGENDIAN
{"RGB", "RGB;16N", 48, unpackRGB16B}, {"RGB", "RGB;16N", 48, unpackRGB16B},
{"RGB", "RGBX;16N", 64, unpackRGBA16B},
{"RGBA", "RGBa;16N", 64, unpackRGBa16B}, {"RGBA", "RGBa;16N", 64, unpackRGBa16B},
{"RGBA", "RGBA;16N", 64, unpackRGBA16B}, {"RGBA", "RGBA;16N", 64, unpackRGBA16B},
{"RGBX", "RGBX;16N", 64, unpackRGBA16B}, {"RGBX", "RGBX;16N", 64, unpackRGBA16B},
@ -1708,6 +1710,7 @@ static struct {
{"RGBA", "A;16N", 16, band316B}, {"RGBA", "A;16N", 16, band316B},
#else #else
{"RGB", "RGB;16N", 48, unpackRGB16L}, {"RGB", "RGB;16N", 48, unpackRGB16L},
{"RGB", "RGBX;16N", 64, unpackRGBA16L},
{"RGBA", "RGBa;16N", 64, unpackRGBa16L}, {"RGBA", "RGBa;16N", 64, unpackRGBa16L},
{"RGBA", "RGBA;16N", 64, unpackRGBA16L}, {"RGBA", "RGBA;16N", 64, unpackRGBA16L},
{"RGBX", "RGBX;16N", 64, unpackRGBA16L}, {"RGBX", "RGBX;16N", 64, unpackRGBA16L},

View File

@ -116,14 +116,13 @@ V = {
"HARFBUZZ": "10.1.0", "HARFBUZZ": "10.1.0",
"JPEGTURBO": "3.1.0", "JPEGTURBO": "3.1.0",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.44", "LIBPNG": "1.6.45",
"LIBWEBP": "1.5.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",
"ZLIBNG": "2.2.2", "ZLIBNG": "2.2.3",
} }
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2])
@ -241,8 +240,8 @@ DEPS: dict[str, dict[str, Any]] = {
}, },
"libpng": { "libpng": {
"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"FILENAME/download",
"filename": f"lpng{V['LIBPNG_DOTLESS']}.zip", "filename": f"libpng-{V['LIBPNG']}.tar.gz",
"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"),