diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt
index 0e314b8bf..520b6e320 100644
--- a/.ci/requirements-cibw.txt
+++ b/.ci/requirements-cibw.txt
@@ -1 +1 @@
-cibuildwheel==2.23.3
+cibuildwheel==3.0.0
diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt
index 86ac2e0b2..44b5badab 100644
--- a/.ci/requirements-mypy.txt
+++ b/.ci/requirements-mypy.txt
@@ -1,4 +1,4 @@
-mypy==1.15.0
+mypy==1.16.1
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 6b76351b0..6d8acc44f 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"]
architecture: ["x64"]
include:
# Test the oldest Python on 32-bit
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 006d574f3..b4b516228 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -43,6 +43,7 @@ jobs:
python-version: [
"pypy3.11",
"pypy3.10",
+ "3.14t",
"3.14",
"3.13t",
"3.13",
@@ -55,6 +56,7 @@ jobs:
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded
+ - { python-version: "3.14t", disable-gil: true }
- { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" }
diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh
index 1583435c1..996d32bc2 100755
--- a/.github/workflows/wheels-dependencies.sh
+++ b/.github/workflows/wheels-dependencies.sh
@@ -39,8 +39,8 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.3
HARFBUZZ_VERSION=11.2.1
-LIBPNG_VERSION=1.6.48
-JPEGTURBO_VERSION=3.1.0
+LIBPNG_VERSION=1.6.49
+JPEGTURBO_VERSION=3.1.1
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.8.1
TIFF_VERSION=4.7.0
diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1
index a1edc14ef..54e7fbbfc 100644
--- a/.github/workflows/wheels-test.ps1
+++ b/.github/workflows/wheels-test.ps1
@@ -9,17 +9,21 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
}
$env:path += ";$pillow\winbuild\build\bin\"
-& "$venv\Scripts\activate.ps1"
+if (Test-Path $venv\Scripts\pypy.exe) {
+ $python = "pypy.exe"
+} else {
+ $python = "python.exe"
+}
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
- & python -m pip install numpy
+ & $venv\Scripts\$python -m pip install numpy
}
cd $pillow
-& python -VV
+& $venv\Scripts\$python -VV
if (!$?) { exit $LASTEXITCODE }
-& python selftest.py
+& $venv\Scripts\$python selftest.py
if (!$?) { exit $LASTEXITCODE }
-& python -m pytest -vx Tests\check_wheel.py
+& $venv\Scripts\$python -m pytest -vx Tests\check_wheel.py
if (!$?) { exit $LASTEXITCODE }
-& python -m pytest -vx Tests
+& $venv\Scripts\$python -m pytest -vx Tests
if (!$?) { exit $LASTEXITCODE }
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 33e1976f0..72516651f 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -110,7 +110,6 @@ jobs:
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
- CIBW_SKIP: pp39-*
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4
@@ -188,7 +187,6 @@ jobs:
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
- CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py
index 8ba40ba3f..9602410da 100644
--- a/Tests/check_wheel.py
+++ b/Tests/check_wheel.py
@@ -11,13 +11,14 @@ from .helper import is_pypy
def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
- # tkinter is not available in cibuildwheel installed CPython on Windows
- try:
- import tkinter
+ if sys.platform == "win32":
+ # tkinter is not available in cibuildwheel installed CPython on Windows
+ try:
+ import tkinter
- assert tkinter
- except ImportError:
- expected_modules.remove("tkinter")
+ assert tkinter
+ except ImportError:
+ expected_modules.remove("tkinter")
assert set(features.get_supported_modules()) == expected_modules
diff --git a/Tests/images/imagedraw_rectangle_I.tiff b/Tests/images/imagedraw_rectangle_I.tiff
index 9b9eda883..f0cb534b6 100644
Binary files a/Tests/images/imagedraw_rectangle_I.tiff and b/Tests/images/imagedraw_rectangle_I.tiff differ
diff --git a/Tests/images/op_index.qoi b/Tests/images/op_index.qoi
new file mode 100644
index 000000000..e626aafe6
Binary files /dev/null and b/Tests/images/op_index.qoi differ
diff --git a/Tests/images/p_4_planes.pcx b/Tests/images/p_4_planes.pcx
new file mode 100644
index 000000000..8c5743a98
Binary files /dev/null and b/Tests/images/p_4_planes.pcx differ
diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py
index 82ff14181..88479ff0d 100644
--- a/Tests/test_deprecate.py
+++ b/Tests/test_deprecate.py
@@ -47,7 +47,6 @@ def test_unknown_version() -> None:
],
)
def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
- expected = r""
with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural)
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index 9f50df22d..5f6b263a1 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -7,9 +7,8 @@ import pytest
from PIL import BlpImagePlugin, Image
from .helper import (
- assert_image_equal,
assert_image_equal_tofile,
- assert_image_similar,
+ assert_image_similar_tofile,
hopper,
)
@@ -52,18 +51,16 @@ def test_save(tmp_path: Path) -> None:
im = hopper("P")
im.save(f, blp_version=version)
- with Image.open(f) as reloaded:
- assert_image_equal(im.convert("RGB"), reloaded)
+ assert_image_equal_tofile(im.convert("RGB"), f)
with Image.open("Tests/images/transparent.png") as im:
f = tmp_path / "temp.blp"
im.convert("P").save(f, blp_version=version)
- with Image.open(f) as reloaded:
- assert_image_similar(im, reloaded, 8)
+ assert_image_similar_tofile(im, f, 8)
im = hopper()
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match="Unsupported BLP image mode"):
im.save(f)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index b9eec591d..2827937cf 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -145,14 +145,16 @@ class TestFileJpeg:
assert k > 0.9
# roundtrip, and check again
im = self.roundtrip(im)
- c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
+ cmyk = im.getpixel((0, 0))
+ assert isinstance(cmyk, tuple)
+ c, m, y, k = (x / 255.0 for x in cmyk)
assert c == 0.0
assert m > 0.8
assert y > 0.8
assert k == 0.0
- c, m, y, k = (
- x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
- )
+ cmyk = im.getpixel((im.size[0] - 1, im.size[1] - 1))
+ assert isinstance(cmyk, tuple)
+ k = cmyk[3] / 255.0
assert k > 0.9
def test_rgb(self) -> None:
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index 5d7fd1c1b..2e999eff6 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -37,6 +37,11 @@ def test_sanity(tmp_path: Path) -> None:
im.save(f)
+def test_p_4_planes() -> None:
+ with Image.open("Tests/images/p_4_planes.pcx") as im:
+ assert im.getpixel((0, 0)) == 3
+
+
def test_bad_image_size() -> None:
with open("Tests/images/pil184.pcx", "rb") as fp:
data = fp.read()
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 0f0886ab8..15f67385a 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -100,11 +100,11 @@ class TestFilePng:
assert im.format == "PNG"
assert im.get_format_mimetype() == "image/png"
- for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
+ for mode in ["1", "L", "P", "RGB", "I;16", "I;16B"]:
im = hopper(mode)
im.save(test_file)
with Image.open(test_file) as reloaded:
- if mode in ("I", "I;16B"):
+ if mode == "I;16B":
reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im)
@@ -801,6 +801,16 @@ class TestFilePng:
with Image.open("Tests/images/truncated_end_chunk.png") as im:
assert_image_equal_tofile(im, "Tests/images/hopper.png")
+ def test_deprecation(self, tmp_path: Path) -> None:
+ test_file = tmp_path / "out.png"
+
+ im = hopper("I")
+ with pytest.warns(DeprecationWarning):
+ im.save(test_file)
+
+ with Image.open(test_file) as reloaded:
+ assert_image_equal(im, reloaded.convert("I"))
+
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib")
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index 41e2b5416..68f2f9468 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -288,14 +288,16 @@ def test_non_integer_token(tmp_path: Path) -> None:
pass
-def test_header_token_too_long(tmp_path: Path) -> None:
+@pytest.mark.parametrize("data", (b"P3\x0cAAAAAAAAAA\xee", b"P6\n 01234567890"))
+def test_header_token_too_long(tmp_path: Path, data: bytes) -> None:
path = tmp_path / "temp.ppm"
with open(path, "wb") as f:
- f.write(b"P6\n 01234567890")
+ f.write(data)
- with pytest.raises(ValueError, match="Token too long in file header: 01234567890"):
+ with pytest.raises(ValueError) as e:
with Image.open(path):
pass
+ assert "Token too long in file header: " in repr(e)
def test_truncated_file(tmp_path: Path) -> None:
diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py
index fd4b981ce..b9becb24f 100644
--- a/Tests/test_file_qoi.py
+++ b/Tests/test_file_qoi.py
@@ -1,10 +1,12 @@
from __future__ import annotations
+from pathlib import Path
+
import pytest
from PIL import Image, QoiImagePlugin
-from .helper import assert_image_equal_tofile
+from .helper import assert_image_equal_tofile, hopper
def test_sanity() -> None:
@@ -28,3 +30,28 @@ def test_invalid_file() -> None:
with pytest.raises(SyntaxError):
QoiImagePlugin.QoiImageFile(invalid_file)
+
+
+def test_op_index() -> None:
+ # QOI_OP_INDEX as the first chunk
+ with Image.open("Tests/images/op_index.qoi") as im:
+ assert im.getpixel((0, 0)) == (0, 0, 0, 0)
+
+
+def test_save(tmp_path: Path) -> None:
+ f = tmp_path / "temp.qoi"
+
+ im = hopper()
+ im.save(f, colorspace="sRGB")
+
+ assert_image_equal_tofile(im, f)
+
+ for path in ("Tests/images/default_font.png", "Tests/images/pil123rgba.png"):
+ with Image.open(path) as im:
+ im.save(f)
+
+ assert_image_equal_tofile(im, f)
+
+ im = hopper("P")
+ with pytest.raises(ValueError, match="Unsupported QOI image mode"):
+ im.save(f)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index d0d394aa9..73046eb5f 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -14,6 +14,7 @@ from PIL import (
ImageFile,
JpegImagePlugin,
TiffImagePlugin,
+ TiffTags,
UnidentifiedImageError,
)
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
@@ -900,6 +901,29 @@ class TestFileTiff:
assert description[0]["format"] == "image/tiff"
assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
+ def test_getxmp_undefined(self, tmp_path: Path) -> None:
+ tmpfile = tmp_path / "temp.tif"
+ im = Image.new("L", (1, 1))
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd.tagtype[700] = TiffTags.UNDEFINED
+ with Image.open("Tests/images/lab.tif") as im_xmp:
+ ifd[700] = im_xmp.info["xmp"]
+ im.save(tmpfile, tiffinfo=ifd)
+
+ with Image.open(tmpfile) as im_reloaded:
+ if ElementTree is None:
+ with pytest.warns(
+ UserWarning,
+ match="XMP data cannot be read without defusedxml dependency",
+ ):
+ assert im_reloaded.getxmp() == {}
+ else:
+ assert "xmp" in im_reloaded.info
+ xmp = im_reloaded.getxmp()
+
+ description = xmp["xmpmeta"]["RDF"]["Description"]
+ assert description[0]["format"] == "image/tiff"
+
def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 14a067127..b018b4309 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -671,6 +671,7 @@ class TestImage:
im_remapped = im.remap_palette(list(range(256)))
assert_image_equal(im, im_remapped)
assert im.palette is not None
+ assert im_remapped.palette is not None
assert im.palette.palette == im_remapped.palette.palette
# Test illegal image mode
@@ -973,6 +974,11 @@ class TestImage:
assert tag not in exif.get_ifd(0x8769)
assert exif.get_ifd(0xA005)
+ def test_exif_from_xmp_bytes(self) -> None:
+ im = Image.new("RGB", (1, 1))
+ im.info["xmp"] = b'\xff tiff:Orientation="2"'
+ assert im.getexif()[274] == 2
+
def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
if ElementTree is None:
@@ -989,7 +995,7 @@ class TestImage:
im = Image.new("RGB", (1, 1))
im.info["xmp"] = (
b'\n'
- b'\n\x00\x00'
+ b'\n\x00\x00 '
)
if ElementTree is None:
with pytest.warns(
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index ffe9c0979..37669a2e5 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -783,9 +783,10 @@ def test_rectangle_I16(bbox: Coords) -> None:
draw = ImageDraw.Draw(im)
# Act
- draw.rectangle(bbox, outline=0xFFFF)
+ draw.rectangle(bbox, outline=0xCDEF)
# Assert
+ assert im.getpixel((X0, Y0)) == 0xCDEF
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff")
diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py
index 073e5415c..976f62384 100644
--- a/Tests/test_tiff_crashes.py
+++ b/Tests/test_tiff_crashes.py
@@ -52,3 +52,17 @@ def test_tiff_crashes(test_file: str) -> None:
pytest.skip("test image not found")
except OSError:
pass
+
+
+def test_tiff_mmap() -> None:
+ try:
+ with Image.open("Tests/images/crash_mmap.tif") as im:
+ im.seek(1)
+ im.load()
+
+ im.seek(0)
+ im.load()
+ except FileNotFoundError:
+ if on_ci():
+ raise
+ pytest.skip("test image not found")
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 2346a3bdc..f4535ac3c 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -193,6 +193,20 @@ Image.Image.get_child_images()
method uses an image's file pointer, and so child images could only be retrieved from
an :py:class:`PIL.ImageFile.ImageFile` instance.
+Saving I mode images as PNG
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. deprecated:: 11.3.0
+
+In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
+at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
+changing the data, this is now deprecated. Instead, the image can be converted to
+another mode before saving::
+
+ from PIL import Image
+ im = Image.new("I", (1, 1))
+ im.convert("I;16").save("out.png")
+
ImageCms.ImageCmsProfile.product_name and .product_info
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index 5ca549c37..a15e84574 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -1082,6 +1082,26 @@ Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
+QOI
+^^^
+
+.. versionadded:: 9.5.0
+
+Pillow reads and writes images in Quite OK Image format using a Python codec. If you
+wish to write code specifically for this format, :pypi:`qoi` is an alternative library
+that uses C to decode the image and interfaces with NumPy.
+
+.. _qoi-saving:
+
+Saving
+~~~~~~
+
+The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
+
+**colorspace**
+ If set to "sRGB", the colorspace will be written as sRGB with linear alpha, instead
+ of all channels being linear.
+
SGI
^^^
@@ -1578,15 +1598,6 @@ PSD
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
-QOI
-^^^
-
-.. versionadded:: 9.5.0
-
-Pillow reads images in Quite OK Image format using a Python decoder. If you wish to
-write code specifically for this format, :pypi:`qoi` is an alternative library that
-uses C to decode the image and interfaces with NumPy.
-
SUN
^^^
diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst
index 57a2298f8..a56f94316 100644
--- a/docs/installation/platform-support.rst
+++ b/docs/installation/platform-support.rst
@@ -40,12 +40,12 @@ These platforms are built and tested for every change.
| macOS 13 Ventura | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
-| | PyPy3 | |
+| | 3.14, PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 |
-| | 3.12, 3.13, PyPy3 | |
+| | 3.12, 3.13, 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 | arm64v8, ppc64le, |
| | | s390x |
@@ -53,7 +53,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.9 | x86 |
| +----------------------------+---------------------+
| | 3.10, 3.11, 3.12, 3.13, | x86-64 |
-| | PyPy3 | |
+| | 3.14, PyPy3 | |
| +----------------------------+---------------------+
| | 3.12 (MinGW) | x86-64 |
| +----------------------------+---------------------+
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 8b2f92323..aac55fe6b 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -18,6 +18,9 @@ OpenType fonts (as well as other font formats supported by the FreeType
library). For earlier versions, TrueType support is only available as part of
the imToolkit package.
+When measuring text sizes, this module will not break at newline characters. For
+multiline text, see the :py:mod:`~PIL.ImageDraw` module.
+
.. warning::
To protect against potential DOS attacks when using arbitrary strings as
text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
diff --git a/docs/releasenotes/11.3.0.rst b/docs/releasenotes/11.3.0.rst
new file mode 100644
index 000000000..d2284c7ec
--- /dev/null
+++ b/docs/releasenotes/11.3.0.rst
@@ -0,0 +1,83 @@
+11.3.0
+------
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+:cve:`YYYY-XXXXX`: TODO
+^^^^^^^^^^^^^^^^^^^^^^^
+
+TODO
+
+Backwards incompatible changes
+==============================
+
+TODO
+^^^^
+
+Deprecations
+============
+
+Saving I mode images as PNG
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In order to fit the 32 bits of I mode images into PNG, when PNG images can only contain
+at most 16 bits for a channel, Pillow has been clipping the values. Rather than quietly
+changing the data, this is now deprecated. Instead, the image can be converted to
+another mode before saving::
+
+ from PIL import Image
+ im = Image.new("I", (1, 1))
+ im.convert("I;16").save("out.png")
+
+API changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API additions
+=============
+
+TODO
+^^^^
+
+TODO
+
+Other changes
+=============
+
+Added QOI saving
+^^^^^^^^^^^^^^^^
+
+Support has been added for saving QOI images. ``colorspace`` can be used to specify the
+colorspace as sRGB with linear alpha, e.g. ``im.save("out.qoi", colorspace="sRGB")``.
+By default, all channels will be linear.
+
+Support using more screenshot utilities with ImageGrab on Linux
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:meth:`~PIL.ImageGrab.grab` is now able to use GNOME Screenshot, grim or Spectacle
+on Linux in order to take a snapshot of the screen.
+
+Do not build against libavif < 1
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pillow only supports libavif 1.0.0 or later. In order to prevent errors when building
+from source, if a user happens to have an earlier libavif on their system, Pillow will
+now ignore it.
+
+Python 3.14 beta
+^^^^^^^^^^^^^^^^
+
+To help other projects prepare for Python 3.14, wheels are now built for the
+3.14 beta as a preview. This is not official support for Python 3.14, but rather
+an opportunity for you to test how Pillow works with the beta and report any
+problems.
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 5d7b21d59..a85f1e075 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 11.3.0
11.2.1
11.1.0
11.0.0
diff --git a/setup.py b/setup.py
index ab36c6b17..3716a7b9f 100644
--- a/setup.py
+++ b/setup.py
@@ -163,7 +163,7 @@ def _find_library_dirs_ldconfig() -> list[str]:
args: list[str]
env: dict[str, str]
expr: str
- if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
+ if sys.platform.startswith(("linux", "gnu")):
if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32"
else:
@@ -623,11 +623,7 @@ class pil_build_ext(build_ext):
for extension in self.extensions:
extension.extra_compile_args = ["-Wno-nullability-completeness"]
- elif (
- sys.platform.startswith("linux")
- or sys.platform.startswith("gnu")
- or sys.platform.startswith("freebsd")
- ):
+ elif sys.platform.startswith(("linux", "gnu", "freebsd")):
for dirname in _find_library_dirs_ldconfig():
_add_directory(library_dirs, dirname)
if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"):
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 4392c4cb9..c98e02f69 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -31,7 +31,7 @@ import os
import subprocess
from enum import IntEnum
from functools import cached_property
-from typing import IO, Any, Literal, NamedTuple, Union
+from typing import IO, Any, Literal, NamedTuple, Union, cast
from . import (
Image,
@@ -350,12 +350,15 @@ class GifImageFile(ImageFile.ImageFile):
if self._frame_palette:
if color * 3 + 3 > len(self._frame_palette.palette):
color = 0
- return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
+ return cast(
+ tuple[int, int, int],
+ tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]),
+ )
else:
return (color, color, color)
self.dispose = None
- self.dispose_extent = frame_dispose_extent
+ self.dispose_extent: tuple[int, int, int, int] | None = frame_dispose_extent
if self.dispose_extent and self.disposal_method >= 2:
try:
if self.disposal_method == 2:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index ed2f728aa..7e9540e48 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1511,7 +1511,7 @@ class Image:
return {}
if "xmp" not in self.info:
return {}
- root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00"))
+ root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 "))
return {get_name(root.tag): get_value(root)}
def getexif(self) -> Exif:
@@ -1542,10 +1542,11 @@ class Image:
# XMP tags
if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
+ pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])'
if not xmp_tags and (xmp_tags := self.info.get("xmp")):
- xmp_tags = xmp_tags.decode("utf-8")
+ pattern = rb'tiff:Orientation(="|>)([0-9])'
if xmp_tags:
- match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
+ match = re.search(pattern, xmp_tags)
if match:
self._exif[ExifTags.Base.Orientation] = int(match[2])
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index e6c7b0298..98ae67539 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -365,22 +365,10 @@ class ImageDraw:
# use the fill as a mask
mask = Image.new("1", self.im.size)
mask_ink = self._getink(1)[0]
-
- fill_im = mask.copy()
- draw = Draw(fill_im)
+ draw = Draw(mask)
draw.draw.draw_polygon(xy, mask_ink, 1)
- ink_im = mask.copy()
- draw = Draw(ink_im)
- width = width * 2 - 1
- draw.draw.draw_polygon(xy, mask_ink, 0, width)
-
- mask.paste(ink_im, mask=fill_im)
-
- im = Image.new(self.mode, self.im.size)
- draw = Draw(im)
- draw.draw.draw_polygon(xy, ink, 0, width)
- self.im.paste(im.im, (0, 0) + im.size, mask.im)
+ self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
def regular_polygon(
self,
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index c29350b7a..1eb450734 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -134,10 +134,10 @@ def grabclipboard() -> Image.Image | list[str] | None:
import struct
o = struct.unpack_from("I", data)[0]
- if data[16] != 0:
- files = data[o:].decode("utf-16le").split("\0")
- else:
+ if data[16] == 0:
files = data[o:].decode("mbcs").split("\0")
+ else:
+ files = data[o:].decode("utf-16le").split("\0")
return files[: files.index("")]
if isinstance(data, bytes):
data = io.BytesIO(data)
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 969528841..defe9f773 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -762,8 +762,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")
- if xmp:
+ if xmp := info.get("xmp"):
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
if len(xmp) > max_data_bytes_in_marker:
@@ -772,8 +771,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
size = o16(2 + overhead_len + len(xmp))
extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
- icc_profile = info.get("icc_profile")
- if icc_profile:
+ if icc_profile := info.get("icc_profile"):
overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
markers = []
@@ -831,7 +829,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
# channels*size, this is a value that's been used in a django patch.
# https://github.com/matthewwithanm/django-imagekit/issues/50
- bufsize = 0
if optimize or progressive:
# CMYK can be bigger
if im.mode == "CMYK":
@@ -848,7 +845,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
else:
# The EXIF info needs to be written as one block, + APP1, + one spare byte.
# Ensure that our buffer is big enough. Same with the icc_profile block.
- bufsize = max(bufsize, len(exif) + 5, len(extra) + 1)
+ bufsize = max(len(exif) + 5, len(extra) + 1)
ImageFile._save(
im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 299405ae0..458d586c4 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -54,7 +54,7 @@ class PcxImageFile(ImageFile.ImageFile):
# header
assert self.fp is not None
- s = self.fp.read(128)
+ s = self.fp.read(68)
if not _accept(s):
msg = "not a PCX file"
raise SyntaxError(msg)
@@ -66,6 +66,8 @@ class PcxImageFile(ImageFile.ImageFile):
raise SyntaxError(msg)
logger.debug("BBox: %s %s %s %s", *bbox)
+ offset = self.fp.tell() + 60
+
# format
version = s[1]
bits = s[3]
@@ -102,7 +104,6 @@ class PcxImageFile(ImageFile.ImageFile):
break
if mode == "P":
self.palette = ImagePalette.raw("RGB", s[1:])
- self.fp.seek(128)
elif version == 5 and bits == 8 and planes == 3:
mode = "RGB"
@@ -128,9 +129,7 @@ class PcxImageFile(ImageFile.ImageFile):
bbox = (0, 0) + self.size
logger.debug("size: %sx%s", *self.size)
- self.tile = [
- ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))
- ]
+ self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))]
# --------------------------------------------------------------------
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index f3815a122..1b9a89aef 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -48,6 +48,7 @@ from ._binary import i32be as i32
from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32
+from ._deprecate import deprecate
from ._util import DeferredError
TYPE_CHECKING = False
@@ -1368,6 +1369,8 @@ def _save(
except KeyError as e:
msg = f"cannot write mode {mode} as PNG"
raise OSError(msg) from e
+ if outmode == "I":
+ deprecate("Saving I mode images as PNG", 13, stacklevel=4)
#
# write minimal PNG file
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 03afa2d2e..db34d107a 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -94,8 +94,8 @@ class PpmImageFile(ImageFile.ImageFile):
msg = "Reached EOF while reading header"
raise ValueError(msg)
elif len(token) > 10:
- msg = f"Token too long in file header: {token.decode()}"
- raise ValueError(msg)
+ msg_too_long = b"Token too long in file header: %s" % token
+ raise ValueError(msg_too_long)
return token
def _open(self) -> None:
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
index df552243e..dba5d809f 100644
--- a/src/PIL/QoiImagePlugin.py
+++ b/src/PIL/QoiImagePlugin.py
@@ -8,9 +8,12 @@
from __future__ import annotations
import os
+from typing import IO
from . import Image, ImageFile
from ._binary import i32be as i32
+from ._binary import o8
+from ._binary import o32be as o32
def _accept(prefix: bytes) -> bool:
@@ -51,7 +54,7 @@ class QoiDecoder(ImageFile.PyDecoder):
assert self.fd is not None
self._previously_seen_pixels = {}
- self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
+ self._previous_pixel = bytearray((0, 0, 0, 255))
data = bytearray()
bands = Image.getmodebands(self.mode)
@@ -110,6 +113,122 @@ class QoiDecoder(ImageFile.PyDecoder):
return -1, 0
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode == "RGB":
+ channels = 3
+ elif im.mode == "RGBA":
+ channels = 4
+ else:
+ msg = "Unsupported QOI image mode"
+ raise ValueError(msg)
+
+ colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1
+
+ fp.write(b"qoif")
+ fp.write(o32(im.size[0]))
+ fp.write(o32(im.size[1]))
+ fp.write(o8(channels))
+ fp.write(o8(colorspace))
+
+ ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)])
+
+
+class QoiEncoder(ImageFile.PyEncoder):
+ _pushes_fd = True
+ _previous_pixel: tuple[int, int, int, int] | None = None
+ _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {}
+ _run = 0
+
+ def _write_run(self) -> bytes:
+ data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN
+ self._run = 0
+ return data
+
+ def _delta(self, left: int, right: int) -> int:
+ result = (left - right) & 255
+ if result >= 128:
+ result -= 256
+ return result
+
+ def encode(self, bufsize: int) -> tuple[int, int, bytes]:
+ assert self.im is not None
+
+ self._previously_seen_pixels = {0: (0, 0, 0, 0)}
+ self._previous_pixel = (0, 0, 0, 255)
+
+ data = bytearray()
+ w, h = self.im.size
+ bands = Image.getmodebands(self.mode)
+
+ for y in range(h):
+ for x in range(w):
+ pixel = self.im.getpixel((x, y))
+ if bands == 3:
+ pixel = (*pixel, 255)
+
+ if pixel == self._previous_pixel:
+ self._run += 1
+ if self._run == 62:
+ data += self._write_run()
+ else:
+ if self._run:
+ data += self._write_run()
+
+ r, g, b, a = pixel
+ hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
+ if self._previously_seen_pixels.get(hash_value) == pixel:
+ data += o8(hash_value) # QOI_OP_INDEX
+ elif self._previous_pixel:
+ self._previously_seen_pixels[hash_value] = pixel
+
+ prev_r, prev_g, prev_b, prev_a = self._previous_pixel
+ if prev_a == a:
+ delta_r = self._delta(r, prev_r)
+ delta_g = self._delta(g, prev_g)
+ delta_b = self._delta(b, prev_b)
+
+ if (
+ -2 <= delta_r < 2
+ and -2 <= delta_g < 2
+ and -2 <= delta_b < 2
+ ):
+ data += o8(
+ 0b01000000
+ | (delta_r + 2) << 4
+ | (delta_g + 2) << 2
+ | (delta_b + 2)
+ ) # QOI_OP_DIFF
+ else:
+ delta_gr = self._delta(delta_r, delta_g)
+ delta_gb = self._delta(delta_b, delta_g)
+ if (
+ -8 <= delta_gr < 8
+ and -32 <= delta_g < 32
+ and -8 <= delta_gb < 8
+ ):
+ data += o8(
+ 0b10000000 | (delta_g + 32)
+ ) # QOI_OP_LUMA
+ data += o8((delta_gr + 8) << 4 | (delta_gb + 8))
+ else:
+ data += o8(0b11111110) # QOI_OP_RGB
+ data += bytes(pixel[:3])
+ else:
+ data += o8(0b11111111) # QOI_OP_RGBA
+ data += bytes(pixel)
+
+ self._previous_pixel = pixel
+
+ if self._run:
+ data += self._write_run()
+ data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding
+
+ return len(data), 0, data
+
+
Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
Image.register_decoder("qoi", QoiDecoder)
Image.register_extension(QoiImageFile.format, ".qoi")
+
+Image.register_save(QoiImageFile.format, _save)
+Image.register_encoder("qoi", QoiEncoder)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 88af9162e..946fbd531 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1217,9 +1217,10 @@ class TiffImageFile(ImageFile.ImageFile):
return
self._seek(frame)
if self._im is not None and (
- self.im.size != self._tile_size or self.im.mode != self.mode
+ self.im.size != self._tile_size
+ or self.im.mode != self.mode
+ or self.readonly
):
- # The core image will no longer be used
self._im = None
def _seek(self, frame: int) -> None:
@@ -1259,7 +1260,10 @@ class TiffImageFile(ImageFile.ImageFile):
self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp)
if XMP in self.tag_v2:
- self.info["xmp"] = self.tag_v2[XMP]
+ xmp = self.tag_v2[XMP]
+ if isinstance(xmp, tuple) and len(xmp) == 1:
+ xmp = xmp[0]
+ self.info["xmp"] = xmp
elif "xmp" in self.info:
del self.info["xmp"]
self._reload_exif()
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 9f9d8bbc9..170d44490 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -12,6 +12,7 @@ def deprecate(
*,
action: str | None = None,
plural: bool = False,
+ stacklevel: int = 3,
) -> None:
"""
Deprecations helper.
@@ -67,5 +68,5 @@ def deprecate(
warnings.warn(
f"{deprecated} {is_} deprecated and will be removed in {removed}{action}",
DeprecationWarning,
- stacklevel=3,
+ stacklevel=stacklevel,
)
diff --git a/src/_imaging.c b/src/_imaging.c
index 9213ba13d..6241dc3ca 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -338,12 +338,6 @@ static const char *no_palette = "image has no palette";
static const char *readonly = "image is readonly";
/* static const char* no_content = "image has no content"; */
-void *
-ImagingError_OSError(void) {
- PyErr_SetString(PyExc_OSError, "error when accessing file");
- return NULL;
-}
-
void *
ImagingError_MemoryError(void) {
return PyErr_NoMemory();
@@ -369,11 +363,6 @@ ImagingError_ValueError(const char *message) {
return NULL;
}
-void
-ImagingError_Clear(void) {
- PyErr_Clear();
-}
-
/* -------------------------------------------------------------------- */
/* HELPERS */
/* -------------------------------------------------------------------- */
@@ -3220,7 +3209,8 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) {
(int)p[3],
&ink,
width,
- self->blend
+ self->blend,
+ NULL
) < 0) {
free(xy);
return NULL;
@@ -3358,7 +3348,10 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
int ink;
int fill = 0;
int width = 0;
- if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) {
+ ImagingObject *maskp = NULL;
+ if (!PyArg_ParseTuple(
+ args, "Oi|iiO!", &data, &ink, &fill, &width, &Imaging_Type, &maskp
+ )) {
return NULL;
}
@@ -3388,8 +3381,16 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
free(xy);
- if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) <
- 0) {
+ if (ImagingDrawPolygon(
+ self->image->image,
+ n,
+ ixy,
+ &ink,
+ fill,
+ width,
+ self->blend,
+ maskp ? maskp->image : NULL
+ ) < 0) {
free(ixy);
return NULL;
}
diff --git a/src/libImaging/Arrow.c b/src/libImaging/Arrow.c
index 0b8c89a07..ccafe33b9 100644
--- a/src/libImaging/Arrow.c
+++ b/src/libImaging/Arrow.c
@@ -101,7 +101,7 @@ export_imaging_schema(Imaging im, struct ArrowSchema *schema) {
}
/* for now, single block images */
- if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
+ if (im->blocks_count > 1) {
return IMAGING_ARROW_MEMORY_LAYOUT;
}
@@ -159,7 +159,7 @@ export_single_channel_array(Imaging im, struct ArrowArray *array) {
int length = im->xsize * im->ysize;
/* for now, single block images */
- if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
+ if (im->blocks_count > 1) {
return IMAGING_ARROW_MEMORY_LAYOUT;
}
@@ -202,7 +202,7 @@ export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
int length = im->xsize * im->ysize;
/* for now, single block images */
- if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
+ if (im->blocks_count > 1) {
return IMAGING_ARROW_MEMORY_LAYOUT;
}
diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c
index d5aff8709..27cac687e 100644
--- a/src/libImaging/Draw.c
+++ b/src/libImaging/Draw.c
@@ -63,7 +63,7 @@ typedef struct {
} Edge;
/* Type used in "polygon*" functions */
-typedef void (*hline_handler)(Imaging, int, int, int, int);
+typedef void (*hline_handler)(Imaging, int, int, int, int, Imaging);
static inline void
point8(Imaging im, int x, int y, int ink) {
@@ -103,9 +103,7 @@ point32rgba(Imaging im, int x, int y, int ink) {
}
static inline void
-hline8(Imaging im, int x0, int y0, int x1, int ink) {
- int pixelwidth;
-
+hline8(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
if (y0 >= 0 && y0 < im->ysize) {
if (x0 < 0) {
x0 = 0;
@@ -118,16 +116,41 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) {
x1 = im->xsize - 1;
}
if (x0 <= x1) {
- pixelwidth = strncmp(im->mode, "I;16", 4) == 0 ? 2 : 1;
- memset(
- im->image8[y0] + x0 * pixelwidth, (UINT8)ink, (x1 - x0 + 1) * pixelwidth
- );
+ int bigendian = -1;
+ if (strncmp(im->mode, "I;16", 4) == 0) {
+ bigendian =
+ (
+#ifdef WORDS_BIGENDIAN
+ strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16L") == 0
+#else
+ strcmp(im->mode, "I;16B") == 0
+#endif
+ )
+ ? 1
+ : 0;
+ }
+ if (mask == NULL && bigendian == -1) {
+ memset(im->image8[y0] + x0, (UINT8)ink, (x1 - x0 + 1));
+ } else {
+ UINT8 *p = im->image8[y0];
+ while (x0 <= x1) {
+ if (mask == NULL || mask->image8[y0][x0]) {
+ if (bigendian == -1) {
+ p[x0] = ink;
+ } else {
+ p[x0 * 2 + (bigendian ? 1 : 0)] = ink;
+ p[x0 * 2 + (bigendian ? 0 : 1)] = ink >> 8;
+ }
+ }
+ x0++;
+ }
+ }
}
}
}
static inline void
-hline32(Imaging im, int x0, int y0, int x1, int ink) {
+hline32(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
INT32 *p;
if (y0 >= 0 && y0 < im->ysize) {
@@ -143,13 +166,16 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) {
}
p = im->image32[y0];
while (x0 <= x1) {
- p[x0++] = ink;
+ if (mask == NULL || mask->image8[y0][x0]) {
+ p[x0] = ink;
+ }
+ x0++;
}
}
}
static inline void
-hline32rgba(Imaging im, int x0, int y0, int x1, int ink) {
+hline32rgba(Imaging im, int x0, int y0, int x1, int ink, Imaging mask) {
unsigned int tmp;
if (y0 >= 0 && y0 < im->ysize) {
@@ -167,9 +193,11 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) {
UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4;
UINT8 *in = (UINT8 *)&ink;
while (x0 <= x1) {
- out[0] = BLEND(in[3], out[0], in[0], tmp);
- out[1] = BLEND(in[3], out[1], in[1], tmp);
- out[2] = BLEND(in[3], out[2], in[2], tmp);
+ if (mask == NULL || mask->image8[y0][x0]) {
+ out[0] = BLEND(in[3], out[0], in[0], tmp);
+ out[1] = BLEND(in[3], out[1], in[1], tmp);
+ out[2] = BLEND(in[3], out[2], in[2], tmp);
+ }
x0++;
out += 4;
}
@@ -407,7 +435,14 @@ x_cmp(const void *x0, const void *x1) {
static void
draw_horizontal_lines(
- Imaging im, int n, Edge *e, int ink, int *x_pos, int y, hline_handler hline
+ Imaging im,
+ int n,
+ Edge *e,
+ int ink,
+ int *x_pos,
+ int y,
+ hline_handler hline,
+ Imaging mask
) {
int i;
for (i = 0; i < n; i++) {
@@ -429,7 +464,7 @@ draw_horizontal_lines(
}
}
- (*hline)(im, xmin, e[i].ymin, xmax, ink);
+ (*hline)(im, xmin, e[i].ymin, xmax, ink, mask);
*x_pos = xmax + 1;
}
}
@@ -440,7 +475,7 @@ draw_horizontal_lines(
*/
static inline int
polygon_generic(
- Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha
+ Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, Imaging mask
) {
Edge **edge_table;
float *xx;
@@ -461,6 +496,7 @@ polygon_generic(
return -1;
}
+ int hasAlpha = hline == hline32rgba;
for (i = 0; i < n; i++) {
if (ymin > e[i].ymin) {
ymin = e[i].ymin;
@@ -470,7 +506,7 @@ polygon_generic(
}
if (e[i].ymin == e[i].ymax) {
if (hasAlpha != 1) {
- (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink);
+ (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink, mask);
}
continue;
}
@@ -558,7 +594,7 @@ polygon_generic(
// Line would be before the current position
continue;
}
- draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
+ draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask);
if (x_end < x_pos) {
// Line would be before the current position
continue;
@@ -574,13 +610,13 @@ polygon_generic(
continue;
}
}
- (*hline)(im, x_start, ymin, x_end, ink);
+ (*hline)(im, x_start, ymin, x_end, ink, mask);
x_pos = x_end + 1;
}
- draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
+ draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline, mask);
} else {
for (i = 1; i < j; i += 2) {
- (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink);
+ (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink, mask);
}
}
}
@@ -590,21 +626,6 @@ polygon_generic(
return 0;
}
-static inline int
-polygon8(Imaging im, int n, Edge *e, int ink, int eofill) {
- return polygon_generic(im, n, e, ink, eofill, hline8, 0);
-}
-
-static inline int
-polygon32(Imaging im, int n, Edge *e, int ink, int eofill) {
- return polygon_generic(im, n, e, ink, eofill, hline32, 0);
-}
-
-static inline int
-polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) {
- return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1);
-}
-
static inline void
add_edge(Edge *e, int x0, int y0, int x1, int y1) {
/* printf("edge %d %d %d %d\n", x0, y0, x1, y1); */
@@ -639,14 +660,13 @@ add_edge(Edge *e, int x0, int y0, int x1, int y1) {
typedef struct {
void (*point)(Imaging im, int x, int y, int ink);
- void (*hline)(Imaging im, int x0, int y0, int x1, int ink);
+ void (*hline)(Imaging im, int x0, int y0, int x1, int ink, Imaging mask);
void (*line)(Imaging im, int x0, int y0, int x1, int y1, int ink);
- int (*polygon)(Imaging im, int n, Edge *e, int ink, int eofill);
} DRAW;
-DRAW draw8 = {point8, hline8, line8, polygon8};
-DRAW draw32 = {point32, hline32, line32, polygon32};
-DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba};
+DRAW draw8 = {point8, hline8, line8};
+DRAW draw32 = {point32, hline32, line32};
+DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba};
/* -------------------------------------------------------------------- */
/* Interface */
@@ -691,7 +711,15 @@ ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink_, in
int
ImagingDrawWideLine(
- Imaging im, int x0, int y0, int x1, int y1, const void *ink_, int width, int op
+ Imaging im,
+ int x0,
+ int y0,
+ int x1,
+ int y1,
+ const void *ink_,
+ int width,
+ int op,
+ Imaging mask
) {
DRAW *draw;
INT32 ink;
@@ -731,7 +759,7 @@ ImagingDrawWideLine(
add_edge(e + 2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]);
add_edge(e + 3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]);
- draw->polygon(im, 4, e, ink, 0);
+ polygon_generic(im, 4, e, ink, 0, draw->hline, mask);
}
return 0;
}
@@ -774,7 +802,7 @@ ImagingDrawRectangle(
}
for (y = y0; y <= y1; y++) {
- draw->hline(im, x0, y, x1, ink);
+ draw->hline(im, x0, y, x1, ink, NULL);
}
} else {
@@ -783,8 +811,8 @@ ImagingDrawRectangle(
width = 1;
}
for (i = 0; i < width; i++) {
- draw->hline(im, x0, y0 + i, x1, ink);
- draw->hline(im, x0, y1 - i, x1, ink);
+ draw->hline(im, x0, y0 + i, x1, ink, NULL);
+ draw->hline(im, x0, y1 - i, x1, ink, NULL);
draw->line(im, x1 - i, y0 + width, x1 - i, y1 - width + 1, ink);
draw->line(im, x0 + i, y0 + width, x0 + i, y1 - width + 1, ink);
}
@@ -795,7 +823,14 @@ ImagingDrawRectangle(
int
ImagingDrawPolygon(
- Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op
+ Imaging im,
+ int count,
+ int *xy,
+ const void *ink_,
+ int fill,
+ int width,
+ int op,
+ Imaging mask
) {
int i, n, x0, y0, x1, y1;
DRAW *draw;
@@ -839,7 +874,7 @@ ImagingDrawPolygon(
if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) {
add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]);
}
- draw->polygon(im, n, e, ink, 0);
+ polygon_generic(im, n, e, ink, 0, draw->hline, mask);
free(e);
} else {
@@ -861,11 +896,12 @@ ImagingDrawPolygon(
xy[i * 2 + 3],
ink_,
width,
- op
+ op,
+ mask
);
}
ImagingDrawWideLine(
- im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op
+ im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op, mask
);
}
}
@@ -1536,7 +1572,9 @@ ellipseNew(
ellipse_init(&st, a, b, width);
int32_t X0, Y, X1;
while (ellipse_next(&st, &X0, &Y, &X1) != -1) {
- draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink);
+ draw->hline(
+ im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL
+ );
}
return 0;
}
@@ -1571,7 +1609,9 @@ clipEllipseNew(
int32_t X0, Y, X1;
int next_code;
while ((next_code = clip_ellipse_next(&st, &X0, &Y, &X1)) >= 0) {
- draw->hline(im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink);
+ draw->hline(
+ im, x0 + (X0 + a) / 2, y0 + (Y + b) / 2, x0 + (X1 + a) / 2, ink, NULL
+ );
}
clip_ellipse_free(&st);
return next_code == -1 ? 0 : -1;
@@ -1989,7 +2029,7 @@ ImagingDrawOutline(
DRAWINIT();
- draw->polygon(im, outline->count, outline->edges, ink, 0);
+ polygon_generic(im, outline->count, outline->edges, ink, 0, draw->hline, NULL);
return 0;
}
diff --git a/src/libImaging/File.c b/src/libImaging/File.c
index 76d0abccc..901fe83ad 100644
--- a/src/libImaging/File.c
+++ b/src/libImaging/File.c
@@ -54,7 +54,7 @@ ImagingSavePPM(Imaging im, const char *outfile) {
fp = fopen(outfile, "wb");
if (!fp) {
- (void)ImagingError_OSError();
+ PyErr_SetString(PyExc_OSError, "error when accessing file");
return 0;
}
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index 234f9943c..bfe67d462 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -270,8 +270,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie);
/* Exceptions */
/* ---------- */
-extern void *
-ImagingError_OSError(void);
extern void *
ImagingError_MemoryError(void);
extern void *
@@ -280,8 +278,6 @@ extern void *
ImagingError_Mismatch(void); /* maps to ValueError by default */
extern void *
ImagingError_ValueError(const char *message);
-extern void
-ImagingError_Clear(void);
/* Transform callbacks */
/* ------------------- */
@@ -510,7 +506,15 @@ extern int
ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, const void *ink, int op);
extern int
ImagingDrawWideLine(
- Imaging im, int x0, int y0, int x1, int y1, const void *ink, int width, int op
+ Imaging im,
+ int x0,
+ int y0,
+ int x1,
+ int y1,
+ const void *ink,
+ int width,
+ int op,
+ Imaging mask
);
extern int
ImagingDrawPieslice(
@@ -530,7 +534,14 @@ extern int
ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op);
extern int
ImagingDrawPolygon(
- Imaging im, int points, int *xy, const void *ink, int fill, int width, int op
+ Imaging im,
+ int points,
+ int *xy,
+ const void *ink,
+ int fill,
+ int width,
+ int op,
+ Imaging mask
);
extern int
ImagingDrawRectangle(
diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c
index 942c8dc22..a65952fb1 100644
--- a/src/libImaging/PcxDecode.c
+++ b/src/libImaging/PcxDecode.c
@@ -60,15 +60,25 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
}
if (state->x >= state->bytes) {
- if (state->bytes % state->xsize && state->bytes > state->xsize) {
- int bands = state->bytes / state->xsize;
- int stride = state->bytes / bands;
+ int bands;
+ int xsize = 0;
+ int stride = 0;
+ if (state->bits == 2 || state->bits == 4) {
+ xsize = (state->xsize + 7) / 8;
+ bands = state->bits;
+ stride = state->bytes / state->bits;
+ } else {
+ xsize = state->xsize;
+ bands = state->bytes / state->xsize;
+ if (bands != 0) {
+ stride = state->bytes / bands;
+ }
+ }
+ if (stride > xsize) {
int i;
for (i = 1; i < bands; i++) { // note -- skipping first band
memmove(
- &state->buffer[i * state->xsize],
- &state->buffer[i * stride],
- state->xsize
+ &state->buffer[i * xsize], &state->buffer[i * stride], xsize
);
}
}
diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c
index 11d6c06cc..6fe26e1bd 100644
--- a/src/libImaging/Storage.c
+++ b/src/libImaging/Storage.c
@@ -645,7 +645,7 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) {
return im;
}
- ImagingError_Clear();
+ PyErr_Clear();
// Try to allocate the image once more with smallest possible block size
MUTEX_LOCK(&ImagingDefaultArena.mutex);
diff --git a/src/map.c b/src/map.c
index c66702981..9a3144ab9 100644
--- a/src/map.c
+++ b/src/map.c
@@ -137,6 +137,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) {
}
}
+ im->read_only = view.readonly;
im->destroy = mapping_destroy_buffer;
Py_INCREF(target);
diff --git a/tox.ini b/tox.ini
index 4065245ee..967d4b537 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,7 @@ requires =
tox>=4.2
env_list =
lint
- py{py3, 313, 312, 311, 310, 39}
+ py{py3, 314, 313, 312, 311, 310, 39}
[testenv]
deps =
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index 6e176e29c..098716b60 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -114,11 +114,11 @@ V = {
"FREETYPE": "2.13.3",
"FRIBIDI": "1.0.16",
"HARFBUZZ": "11.2.1",
- "JPEGTURBO": "3.1.0",
+ "JPEGTURBO": "3.1.1",
"LCMS2": "2.17",
"LIBAVIF": "1.3.0",
"LIBIMAGEQUANT": "4.3.4",
- "LIBPNG": "1.6.48",
+ "LIBPNG": "1.6.49",
"LIBWEBP": "1.5.0",
"OPENJPEG": "2.5.3",
"TIFF": "4.7.0",