Merge branch 'main' into use-ptr

This commit is contained in:
Alexander Karpinsky 2024-09-18 22:24:46 +02:00 committed by GitHub
commit af521a1ce1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 500 additions and 239 deletions

View File

@ -21,7 +21,7 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ ghostscript libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev sway wl-clipboard libopenblas-dev
fi fi
@ -38,12 +38,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports free-threading python3 -m pip install numpy
if [[ "$PYTHON_GIL" == "0" ]]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then

View File

@ -1 +1 @@
cibuildwheel==2.20.0 cibuildwheel==2.21.1

View File

@ -37,7 +37,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ os: [
"macos-14", "macos-latest",
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
@ -56,7 +56,7 @@ jobs:
# M1 only available for 3.10+ # M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" } - { os: "macos-13", python-version: "3.9" }
exclude: exclude:
- { os: "macos-14", python-version: "3.9" } - { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
@ -76,7 +76,7 @@ jobs:
"pyproject.toml" "pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded) - name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0 uses: deadsnakes/action@v3.2.0
if: "${{ matrix.disable-gil }}" if: "${{ matrix.disable-gil }}"
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}

View File

@ -16,11 +16,15 @@ 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.2
HARFBUZZ_VERSION=8.5.0 if [[ "$MB_ML_VER" != 2014 ]]; then
LIBPNG_VERSION=1.6.43 HARFBUZZ_VERSION=9.0.0
JPEGTURBO_VERSION=3.0.3 else
HARFBUZZ_VERSION=8.5.0
fi
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.0.4
OPENJPEG_VERSION=2.5.2 OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5 XZ_VERSION=5.6.2
TIFF_VERSION=4.6.0 TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16 LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
@ -40,7 +44,7 @@ BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg { function build_openjpeg {
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz)
(cd $out_dir \ (cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install) && make install)
@ -50,7 +54,7 @@ fi
function build_brotli { function build_brotli {
local cmake=$(get_modern_cmake) local cmake=$(get_modern_cmake)
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \ (cd $out_dir \
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install) && make install)
@ -60,6 +64,25 @@ function build_brotli {
fi fi
} }
function build_harfbuzz {
if [[ "$HARFBUZZ_VERSION" == 8.5.0 ]]; then
export FREETYPE_LIBS=-lfreetype
export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
export FREETYPE_LIBS=""
export FREETYPE_CFLAGS=""
else
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
(cd $out_dir \
&& meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled)
(cd $out_dir/build \
&& meson install)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libharfbuzz* /usr/local/lib
fi
fi
}
function build { function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
sudo chown -R runner /usr/local sudo chown -R runner /usr/local
@ -109,15 +132,7 @@ function build {
build_freetype build_freetype
fi fi
if [ -z "$IS_MACOS" ]; then build_harfbuzz
export FREETYPE_LIBS=-lfreetype
export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
fi
build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
if [ -z "$IS_MACOS" ]; then
export FREETYPE_LIBS=""
export FREETYPE_CFLAGS=""
fi
} }
# Any stuff that you need to do before you start building the wheels # Any stuff that you need to do before you start building the wheels
@ -140,7 +155,13 @@ if [[ -n "$IS_MACOS" ]]; then
brew remove --ignore-dependencies webp brew remove --ignore-dependencies webp
fi fi
brew install pkg-config brew install meson pkg-config
elif [[ "$MB_ML_LIBC" == "manylinux" ]]; then
if [[ "$HARFBUZZ_VERSION" != 8.5.0 ]]; then
yum install -y meson
fi
else
apk add meson
fi fi
wrap_wheel_builder build wrap_wheel_builder build

View File

@ -13,14 +13,7 @@ else
yum install -y fribidi yum install -y fribidi
fi fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then python3 -m pip install numpy
# TODO Update condition when NumPy supports free-threading
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
fi
if [ ! -d "test-images-main" ]; then if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip

View File

@ -102,12 +102,18 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- name: "macOS x86_64" - name: "macOS 10.10 x86_64"
os: macos-13 os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
build: "pp310* cp3{9,10,11}*"
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS arm64" - name: "macOS arm64"
os: macos-14 os: macos-latest
cibw_arch: arm64 cibw_arch: arm64
macosx_deployment_target: "11.0" macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64" - name: "manylinux2014 and musllinux x86_64"
@ -145,7 +151,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
windows: windows:

View File

@ -5,6 +5,24 @@ Changelog (Pillow)
11.0.0 (unreleased) 11.0.0 (unreleased)
------------------- -------------------
- Accept float stroke widths #8369
[radarhere]
- Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352
[radarhere]
- Improved handling of RGBA palettes when saving GIF images #8366
[radarhere]
- Deprecate isImageType #8364
[radarhere]
- Support converting more modes to LAB by converting to RGBA first #8358
[radarhere]
- Deprecate support for FreeType 2.9.0 #8356
[hugovk, radarhere]
- Removed unused TiffImagePlugin IFD_LEGACY_API #8355 - Removed unused TiffImagePlugin IFD_LEGACY_API #8355
[radarhere] [radarhere]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1429,3 +1429,21 @@ def test_saving_rgba(tmp_path: Path) -> None:
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
reloaded_rgba = reloaded.convert("RGBA") reloaded_rgba = reloaded.convert("RGBA")
assert reloaded_rgba.load()[0, 0][3] == 0 assert reloaded_rgba.load()[0, 0][3] == 0
def test_optimizing_p_rgba(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
im1 = Image.new("P", (100, 100))
d = ImageDraw.Draw(im1)
d.ellipse([(40, 40), (60, 60)], fill=1)
data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254
im1.putpalette(data, "RGBA")
im2 = Image.new("P", (100, 100))
im2.putpalette(data, "RGBA")
im1.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reloaded:
assert reloaded.n_frames == 2

View File

@ -63,8 +63,8 @@ def test_save_append_images(tmp_path: Path) -> None:
assert_image_similar_tofile(im, temp_file, 1) assert_image_similar_tofile(im, temp_file, 1)
with Image.open(temp_file) as reread: with Image.open(temp_file) as reread:
reread.size = (16, 16, 2) reread.size = (16, 16)
reread.load() reread.load(2)
assert_image_equal(reread, provided_im) assert_image_equal(reread, provided_im)
@ -87,14 +87,21 @@ def test_sizes() -> None:
for w, h, r in im.info["sizes"]: for w, h, r in im.info["sizes"]:
wr = w * r wr = w * r
hr = h * r hr = h * r
im.size = (w, h, r) with pytest.warns(DeprecationWarning):
im.size = (w, h, r)
im.load() im.load()
assert im.mode == "RGBA" assert im.mode == "RGBA"
assert im.size == (wr, hr) assert im.size == (wr, hr)
# Test using load() with scale
im.size = (w, h)
im.load(scale=r)
assert im.mode == "RGBA"
assert im.size == (wr, hr)
# Check that we cannot load an incorrect size # Check that we cannot load an incorrect size
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.size = (1, 1) im.size = (1, 2)
def test_older_icon() -> None: def test_older_icon() -> None:
@ -105,8 +112,8 @@ def test_older_icon() -> None:
wr = w * r wr = w * r
hr = h * r hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2: with Image.open("Tests/images/pillow2.icns") as im2:
im2.size = (w, h, r) im2.size = (w, h)
im2.load() im2.load(r)
assert im2.mode == "RGBA" assert im2.mode == "RGBA"
assert im2.size == (wr, hr) assert im2.size == (wr, hr)
@ -122,8 +129,8 @@ def test_jp2_icon() -> None:
wr = w * r wr = w * r
hr = h * r hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2: with Image.open("Tests/images/pillow3.icns") as im2:
im2.size = (w, h, r) im2.size = (w, h)
im2.load() im2.load(r)
assert im2.mode == "RGBA" assert im2.mode == "RGBA"
assert im2.size == (wr, hr) assert im2.size == (wr, hr)

View File

@ -1086,22 +1086,17 @@ class TestImage:
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
""" """
with Image.open(os.path.join("Tests/images", path)) as im: with Image.open(os.path.join("Tests/images", path)) as im:
try: with pytest.raises(OSError) as e:
im.load() im.load()
pytest.fail() buffer_overrun = str(e.value) == "buffer overrun when reading image file"
except OSError as e: truncated = "image file is truncated" in str(e.value)
buffer_overrun = str(e) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e)
assert buffer_overrun or truncated assert buffer_overrun or truncated
def test_fli_overrun2(self) -> None: def test_fli_overrun2(self) -> None:
with Image.open("Tests/images/fli_overrun2.bin") as im: with Image.open("Tests/images/fli_overrun2.bin") as im:
try: with pytest.raises(OSError, match="buffer overrun when reading image file"):
im.seek(1) im.seek(1)
pytest.fail()
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
def test_exit_fp(self) -> None: def test_exit_fp(self) -> None:
with Image.new("L", (1, 1)) as im: with Image.new("L", (1, 1)) as im:
@ -1117,6 +1112,10 @@ class TestImage:
assert len(caplog.records) == 0 assert len(caplog.records) == 0
assert im.fp is None assert im.fp is None
def test_deprecation(self) -> None:
with pytest.warns(DeprecationWarning):
assert not Image.isImageType(None)
class TestImageBytes: class TestImageBytes:
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"]) @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])

View File

@ -49,5 +49,7 @@ def test_copy_zero() -> None:
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_deepcopy() -> None: def test_deepcopy() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
out = copy.deepcopy(im) out = copy.deepcopy(im)
assert out.size == (590, 88) assert out.size == (590, 88)

View File

@ -300,9 +300,7 @@ class TestImageResize:
im.resize((10, 10), "unknown") im.resize((10, 10), "unknown")
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_load_first(self) -> None: def test_transposed(self) -> None:
# load() may change the size of the image
# Test that resize() is calling it before getting the size
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
im = im.resize((64, 64)) im = im.resize((64, 64))
assert im.size == (64, 64) assert im.size == (64, 64)

View File

@ -92,15 +92,13 @@ def test_no_resize() -> None:
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_load_first() -> None: def test_transposed() -> None:
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
im.thumbnail((64, 64)) im.thumbnail((64, 64))
assert im.size == (64, 10) assert im.size == (64, 10)
# Test thumbnail(), without draft(),
# on an image that is large enough once load() has changed the size
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
im.thumbnail((590, 88), reducing_gap=None) im.thumbnail((590, 88), reducing_gap=None)
assert im.size == (590, 88) assert im.size == (590, 88)

View File

@ -696,6 +696,12 @@ def test_rgb_lab(mode: str) -> None:
assert value[:3] == (0, 255, 255) assert value[:3] == (0, 255, 255)
def test_cmyk_lab() -> None:
im = Image.new("CMYK", (1, 1))
converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (255, 128, 128)
def test_deprecation() -> None: def test_deprecation() -> None:
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")

View File

@ -1368,6 +1368,20 @@ def test_stroke() -> None:
) )
@skip_unless_feature("freetype2")
def test_stroke_float() -> None:
# Arrange
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
draw.text((12, 12), "A", "#f00", font, stroke_width=0.5)
# Assert
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_float.png", 3.1)
@skip_unless_feature("freetype2") @skip_unless_feature("freetype2")
def test_stroke_descender() -> None: def test_stroke_descender() -> None:
# Arrange # Arrange

View File

@ -5,6 +5,7 @@ import os
import re import re
import shutil import shutil
import sys import sys
import tempfile
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any, BinaryIO from typing import Any, BinaryIO
@ -460,17 +461,43 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
assert mask.size == (108, 13) assert mask.size == (108, 13)
def test_load_when_image_not_found() -> None:
with tempfile.NamedTemporaryFile(delete=False) as tmp:
pass
with pytest.raises(OSError) as e:
ImageFont.load(tmp.name)
os.unlink(tmp.name)
root = os.path.splitext(tmp.name)[0]
assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}"
def test_load_path_not_found() -> None: def test_load_path_not_found() -> None:
# Arrange # Arrange
filename = "somefilenamethatdoesntexist.ttf" filename = "somefilenamethatdoesntexist.ttf"
# Act/Assert # Act/Assert
with pytest.raises(OSError): with pytest.raises(OSError) as e:
ImageFont.load_path(filename) ImageFont.load_path(filename)
# The file doesn't exist, so don't suggest `load`
assert filename in str(e.value)
assert "did you mean" not in str(e.value)
with pytest.raises(OSError): with pytest.raises(OSError):
ImageFont.truetype(filename) ImageFont.truetype(filename)
def test_load_path_existing_path() -> None:
with tempfile.NamedTemporaryFile() as tmp:
with pytest.raises(OSError) as e:
ImageFont.load_path(tmp.name)
# The file exists, so the error message suggests to use `load` instead
assert tmp.name in str(e.value)
assert " did you mean" in str(e.value)
def test_load_non_font_bytes() -> None: def test_load_non_font_bytes() -> None:
with open("Tests/images/hopper.jpg", "rb") as f: with open("Tests/images/hopper.jpg", "rb") as f:
with pytest.raises(OSError): with pytest.raises(OSError):
@ -1150,3 +1177,15 @@ def test_invalid_truetype_sizes_raise_valueerror(
) -> None: ) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
def test_freetype_deprecation(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: mock features.version_module to return fake FreeType version
def fake_version_module(module: str) -> str:
return "2.9.0"
monkeypatch.setattr(features, "version_module", fake_version_module)
# Act / Assert
with pytest.warns(DeprecationWarning):
ImageFont.truetype(FONT_PATH, FONT_SIZE)

View File

@ -115,7 +115,7 @@ def test_ipythonviewer() -> None:
test_viewer = viewer test_viewer = viewer
break break
else: else:
pytest.fail() pytest.fail("IPythonViewer not found")
im = hopper() im = hopper()
assert test_viewer.show(im) == 1 assert test_viewer.show(im) == 1

View File

@ -60,6 +60,18 @@ class TestImageWinDib:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageWin.Dib(mode) ImageWin.Dib(mode)
def test_dib_hwnd(self) -> None:
mode = "RGBA"
size = (128, 128)
wnd = 0
dib = ImageWin.Dib(mode, size)
hwnd = ImageWin.HWND(wnd)
dib.expose(hwnd)
dib.draw(hwnd, (0, 0) + size)
assert isinstance(dib.query_palette(hwnd), int)
def test_dib_paste(self) -> None: def test_dib_paste(self) -> None:
# Arrange # Arrange
im = hopper() im = hopper()

View File

@ -238,8 +238,10 @@ def test_zero_size() -> None:
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
def test_load_first() -> None: def test_transposed() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im: with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
a = numpy.array(im) a = numpy.array(im)
assert a.shape == (88, 590) assert a.shape == (88, 590)

View File

@ -109,6 +109,35 @@ ImageDraw.getdraw hints parameter
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated. The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
FreeType 2.9.0
^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0
(2025-10-15), when FreeType 2.9.1 will be the minimum supported.
We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe
vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/
ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Setting an ICNS image size to ``(width, height, scale)`` before loading has been
deprecated. Instead, ``load(scale)`` can be used.
Image isImageType()
^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)``
instead.
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -324,12 +324,19 @@ sets the following :py:attr:`~PIL.Image.Image.info` property:
**sizes** **sizes**
A list of supported sizes found in this icon file; these are a A list of supported sizes found in this icon file; these are a
3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina
icon and 1 for a standard icon. You *are* permitted to use this 3-tuple icon and 1 for a standard icon.
format for the :py:attr:`~PIL.Image.Image.size` property if you set it
before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size .. _icns-loading:
will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you
ask for ``(512, 512, 2)``, the final value of Loading
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). ~~~~~~~
You can call the :py:meth:`~PIL.Image.Image.load` method with the following parameter.
**scale**
Affects the scale of the resultant image. If the size is set to ``(512, 512)``,
after loading at scale 2, the final value of :py:attr:`~PIL.Image.Image.size` will
be ``(1024, 1024)``.
.. _icns-saving: .. _icns-saving:

View File

@ -23,6 +23,13 @@ Python 3.8
Pillow has dropped support for Python 3.8, Pillow has dropped support for Python 3.8,
which reached end-of-life in October 2024. which reached end-of-life in October 2024.
Python 3.12 on macOS <= 10.12
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The latest version of Python 3.12 only supports macOS versions 10.13 and later,
and so Pillow has also updated the deployment target for its prebuilt Python 3.12
wheels.
PSFile PSFile
^^^^^^ ^^^^^^
@ -45,12 +52,48 @@ TiffImagePlugin IFD_LEGACY_API
An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed. An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed.
WebP 0.4
^^^^^^^^
Support for WebP 0.4 and earlier has been removed; WebP 0.5 is the minimum supported.
Deprecations Deprecations
============ ============
FreeType 2.9.0
^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Support for FreeType 2.9.0 is deprecated and will be removed in Pillow 12.0.0
(2025-10-15), when FreeType 2.9.1 will be the minimum supported.
We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe
vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/
ICNS (width, height, scale) sizes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
Setting an ICNS image size to ``(width, height, scale)`` before loading has been
deprecated. Instead, ``load(scale)`` can be used.
Image isImageType()
^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``Image.isImageType(im)`` has been deprecated. Use ``isinstance(im, Image.Image)``
instead.
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more
keyword arguments can be used instead. keyword arguments can be used instead.
@ -66,6 +109,8 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and ``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return ``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow ``True`` if the WebP module is installed, until they are removed in Pillow
@ -82,10 +127,18 @@ TODO
API Additions API Additions
============= =============
TODO Writing XMP bytes to JPEG and MPO
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO XMP data can now be saved to JPEG files using an ``xmp`` argument::
im.save("out.jpg", xmp=b"test")
The data can also be set through :py:attr:`~PIL.Image.Image.info`, for use when saving
either JPEG or MPO images::
im.info["xmp"] = b"test"
im.save("out.jpg")
Other Changes Other Changes
============= =============
@ -99,6 +152,10 @@ of 3.13.0 final (2024-10-01, :pep:`719`).
Pillow 11.0.0 now officially supports Python 3.13. Pillow 11.0.0 now officially supports Python 3.13.
Support has also been added for the experimental free-threaded mode of :pep:`703`.
Python 3.13 only supports macOS versions 10.13 and later.
C-level Flags C-level Flags
^^^^^^^^^^^^^ ^^^^^^^^^^^^^

View File

@ -125,7 +125,6 @@ lint.ignore = [
"PT007", # pytest-parametrize-values-wrong-type "PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad "PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements "PT012", # pytest-raises-with-multiple-statements
"PT016", # pytest-fail-without-message
"PT017", # pytest-assert-in-except "PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11 "PYI034", # flake8-pyi: typing.Self added in Python 3.11

View File

@ -553,7 +553,9 @@ def _normalize_palette(
if im.mode == "P": if im.mode == "P":
if not source_palette: if not source_palette:
source_palette = im.im.getpalette("RGB")[:768] im_palette = im.getpalette(None)
assert im_palette is not None
source_palette = bytearray(im_palette)
else: # L-mode else: # L-mode
if not source_palette: if not source_palette:
source_palette = bytearray(i // 3 for i in range(768)) source_palette = bytearray(i // 3 for i in range(768))
@ -629,7 +631,10 @@ def _write_single_frame(
def _getbbox( def _getbbox(
base_im: Image.Image, im_frame: Image.Image base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]: ) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): palette_bytes = [
bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame)
]
if palette_bytes[0] != palette_bytes[1]:
im_frame = im_frame.convert("RGBA") im_frame = im_frame.convert("RGBA")
base_im = base_im.convert("RGBA") base_im = base_im.convert("RGBA")
delta = ImageChops.subtract_modulo(im_frame, base_im) delta = ImageChops.subtract_modulo(im_frame, base_im)
@ -984,7 +989,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
:param im: Image object :param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header :returns: Bytes, len<=768 suitable for inclusion in gif header
""" """
return bytes(im.palette.palette) if im.palette else b"" if not im.palette:
return b""
palette = bytes(im.palette.palette)
if im.palette.mode == "RGBA":
palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))
return palette
def _get_background( def _get_background(

View File

@ -25,6 +25,7 @@ import sys
from typing import IO from typing import IO
from . import Image, ImageFile, PngImagePlugin, features from . import Image, ImageFile, PngImagePlugin, features
from ._deprecate import deprecate
enable_jpeg2k = features.check_codec("jpg_2000") enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k: if enable_jpeg2k:
@ -275,37 +276,37 @@ class IcnsImageFile(ImageFile.ImageFile):
self.best_size[1] * self.best_size[2], self.best_size[1] * self.best_size[2],
) )
@property @property # type: ignore[override]
def size(self): def size(self) -> tuple[int, int] | tuple[int, int, int]:
return self._size return self._size
@size.setter @size.setter
def size(self, value) -> None: def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None:
info_size = value if len(value) == 3:
if info_size not in self.info["sizes"] and len(info_size) == 2: deprecate("Setting size to (width, height, scale)", 12, "load(scale)")
info_size = (info_size[0], info_size[1], 1) if value in self.info["sizes"]:
if ( self._size = value # type: ignore[assignment]
info_size not in self.info["sizes"] return
and len(info_size) == 3 else:
and info_size[2] == 1 # Check that a matching size exists,
): # or that there is a scale that would create a size that matches
simple_sizes = [ for size in self.info["sizes"]:
(size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] simple_size = size[0] * size[2], size[1] * size[2]
] scale = simple_size[0] // value[0]
if value in simple_sizes: if simple_size[1] / value[1] == scale:
info_size = self.info["sizes"][simple_sizes.index(value)] self._size = value
if info_size not in self.info["sizes"]: return
msg = "This is not one of the allowed sizes of this image" msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg) raise ValueError(msg)
self._size = value
def load(self) -> Image.core.PixelAccess | None: def load(self, scale: int | None = None) -> Image.core.PixelAccess | None:
if len(self.size) == 3: if scale is not None or len(self.size) == 3:
self.best_size = self.size if scale is None and len(self.size) == 3:
self.size = ( scale = self.size[2]
self.best_size[0] * self.best_size[2], assert scale is not None
self.best_size[1] * self.best_size[2], width, height = self.size[:2]
) self.size = width * scale, height * scale
self.best_size = width, height, scale
px = Image.Image.load(self) px = Image.Image.load(self)
if self._im is not None and self.im.size == self.size: if self._im is not None and self.im.size == self.size:

View File

@ -133,6 +133,7 @@ def isImageType(t: Any) -> TypeGuard[Image]:
:param t: object to check if it's an image :param t: object to check if it's an image
:returns: True if the object is an image :returns: True if the object is an image
""" """
deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)")
return hasattr(t, "im") return hasattr(t, "im")
@ -1061,7 +1062,7 @@ class Image:
trns_im = new(self.mode, (1, 1)) trns_im = new(self.mode, (1, 1))
if self.mode == "P": if self.mode == "P":
assert self.palette is not None assert self.palette is not None
trns_im.putpalette(self.palette) trns_im.putpalette(self.palette, self.palette.mode)
if isinstance(t, tuple): if isinstance(t, tuple):
err = "Couldn't allocate a palette color for transparency" err = "Couldn't allocate a palette color for transparency"
assert trns_im.palette is not None assert trns_im.palette is not None
@ -1125,17 +1126,23 @@ class Image:
return new_im return new_im
if "LAB" in (self.mode, mode): if "LAB" in (self.mode, mode):
other_mode = mode if self.mode == "LAB" else self.mode im = self
if mode == "LAB":
if im.mode not in ("RGB", "RGBA", "RGBX"):
im = im.convert("RGBA")
other_mode = im.mode
else:
other_mode = mode
if other_mode in ("RGB", "RGBA", "RGBX"): if other_mode in ("RGB", "RGBA", "RGBX"):
from . import ImageCms from . import ImageCms
srgb = ImageCms.createProfile("sRGB") srgb = ImageCms.createProfile("sRGB")
lab = ImageCms.createProfile("LAB") lab = ImageCms.createProfile("LAB")
profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab]
transform = ImageCms.buildTransform( transform = ImageCms.buildTransform(
profiles[0], profiles[1], self.mode, mode profiles[0], profiles[1], im.mode, mode
) )
return transform.apply(self) return transform.apply(im)
# colorspace conversion # colorspace conversion
if dither is None: if dither is None:
@ -1812,23 +1819,22 @@ class Image:
:param mask: An optional mask image. :param mask: An optional mask image.
""" """
if isImageType(box): if isinstance(box, Image):
if mask is not None: if mask is not None:
msg = "If using second argument as mask, third argument must be None" msg = "If using second argument as mask, third argument must be None"
raise ValueError(msg) raise ValueError(msg)
# abbreviated paste(im, mask) syntax # abbreviated paste(im, mask) syntax
mask = box mask = box
box = None box = None
assert not isinstance(box, Image)
if box is None: if box is None:
box = (0, 0) box = (0, 0)
if len(box) == 2: if len(box) == 2:
# upper left corner given; get size from image or mask # upper left corner given; get size from image or mask
if isImageType(im): if isinstance(im, Image):
size = im.size size = im.size
elif isImageType(mask): elif isinstance(mask, Image):
size = mask.size size = mask.size
else: else:
# FIXME: use self.size here? # FIXME: use self.size here?
@ -1841,17 +1847,15 @@ class Image:
from . import ImageColor from . import ImageColor
source = ImageColor.getcolor(im, self.mode) source = ImageColor.getcolor(im, self.mode)
elif isImageType(im): elif isinstance(im, Image):
im.load() im.load()
if self.mode != im.mode: if self.mode != im.mode:
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
# should use an adapter for this! # should use an adapter for this!
im = im.convert(self.mode) im = im.convert(self.mode)
source = im.im source = im.im
elif isinstance(im, tuple):
source = im
else: else:
source = cast(float, im) source = im
self._ensure_mutable() self._ensure_mutable()
@ -2012,7 +2016,7 @@ class Image:
else: else:
band = 3 band = 3
if isImageType(alpha): if isinstance(alpha, Image):
# alpha layer # alpha layer
if alpha.mode not in ("1", "L"): if alpha.mode not in ("1", "L"):
msg = "illegal image mode" msg = "illegal image mode"
@ -2022,7 +2026,6 @@ class Image:
alpha = alpha.convert("L") alpha = alpha.convert("L")
else: else:
# constant alpha # constant alpha
alpha = cast(int, alpha) # see python/typing#1013
try: try:
self.im.fillband(band, alpha) self.im.fillband(band, alpha)
except (AttributeError, ValueError): except (AttributeError, ValueError):
@ -2171,6 +2174,9 @@ class Image:
source_palette = self.im.getpalette(palette_mode, palette_mode) source_palette = self.im.getpalette(palette_mode, palette_mode)
else: # L-mode else: # L-mode
source_palette = bytearray(i // 3 for i in range(768)) source_palette = bytearray(i // 3 for i in range(768))
elif len(source_palette) > 768:
bands = 4
palette_mode = "RGBA"
palette_bytes = b"" palette_bytes = b""
new_positions = [0] * 256 new_positions = [0] * 256
@ -2321,7 +2327,6 @@ class Image:
msg = "reducing_gap must be 1.0 or greater" msg = "reducing_gap must be 1.0 or greater"
raise ValueError(msg) raise ValueError(msg)
self.load()
if box is None: if box is None:
box = (0, 0) + self.size box = (0, 0) + self.size
@ -2770,27 +2775,18 @@ class Image:
) )
return x, y return x, y
box = None preserved_size = preserve_aspect_ratio()
final_size: tuple[int, int] if preserved_size is None:
if reducing_gap is not None: return
preserved_size = preserve_aspect_ratio() final_size = preserved_size
if preserved_size is None:
return
final_size = preserved_size
box = None
if reducing_gap is not None:
res = self.draft( res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
) )
if res is not None: if res is not None:
box = res[1] box = res[1]
if box is None:
self.load()
# load() may have changed the size of the image
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
final_size = preserved_size
if self.size != final_size: if self.size != final_size:
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)

View File

@ -322,7 +322,7 @@ class ImageFile(Image.Image):
def load_prepare(self) -> None: def load_prepare(self) -> None:
# create image memory if necessary # create image memory if necessary
if self._im is None or self.im.mode != self.mode or self.im.size != self.size: if self._im is None:
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
# create palette (optional) # create palette (optional)
if self.mode == "P": if self.mode == "P":

View File

@ -36,7 +36,7 @@ from io import BytesIO
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast
from . import Image from . import Image, features
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
@ -98,11 +98,13 @@ class ImageFont:
def _load_pilfont(self, filename: str) -> None: def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
image: ImageFile.ImageFile | None = None image: ImageFile.ImageFile | None = None
root = os.path.splitext(filename)[0]
for ext in (".png", ".gif", ".pbm"): for ext in (".png", ".gif", ".pbm"):
if image: if image:
image.close() image.close()
try: try:
fullname = os.path.splitext(filename)[0] + ext fullname = root + ext
image = Image.open(fullname) image = Image.open(fullname)
except Exception: except Exception:
pass pass
@ -112,7 +114,8 @@ class ImageFont:
else: else:
if image: if image:
image.close() image.close()
msg = "cannot find glyph data file"
msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
raise OSError(msg) raise OSError(msg)
self.file = fullname self.file = fullname
@ -224,7 +227,7 @@ class FreeTypeFont:
raise core.ex raise core.ex
if size <= 0: if size <= 0:
msg = "font size must be greater than 0" msg = f"font size must be greater than 0, not {size}"
raise ValueError(msg) raise ValueError(msg)
self.path = font self.path = font
@ -232,6 +235,21 @@ class FreeTypeFont:
self.index = index self.index = index
self.encoding = encoding self.encoding = encoding
try:
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if freetype_version := features.version_module("freetype2"):
if parse_version(freetype_version) < parse_version("2.9.1"):
warnings.warn(
"Support for FreeType 2.9.0 is deprecated and will be removed "
"in Pillow 12 (2025-10-15). Please upgrade to FreeType 2.9.1 "
"or newer, preferably FreeType 2.10.4 which fixes "
"CVE-2020-15999.",
DeprecationWarning,
)
if layout_engine not in (Layout.BASIC, Layout.RAQM): if layout_engine not in (Layout.BASIC, Layout.RAQM):
layout_engine = Layout.BASIC layout_engine = Layout.BASIC
if core.HAVE_RAQM: if core.HAVE_RAQM:
@ -768,8 +786,9 @@ class TransposedFont:
def load(filename: str) -> ImageFont: def load(filename: str) -> ImageFont:
""" """
Load a font file. This function loads a font object from the given Load a font file. This function loads a font object from the given
bitmap font file, and returns the corresponding font object. bitmap font file, and returns the corresponding font object. For loading TrueType
or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`.
:param filename: Name of font file. :param filename: Name of font file.
:return: A font object. :return: A font object.
@ -789,9 +808,10 @@ def truetype(
) -> FreeTypeFont: ) -> FreeTypeFont:
""" """
Load a TrueType or OpenType font from a file or file-like object, Load a TrueType or OpenType font from a file or file-like object,
and create a font object. and create a font object. This function loads a font object from the given
This function loads a font object from the given file or file-like file or file-like object, and creates a font object for a font of the given
object, and creates a font object for a font of the given size. size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load`
and :py:func:`~PIL.ImageFont.load_path`.
Pillow uses FreeType to open font files. On Windows, be aware that FreeType Pillow uses FreeType to open font files. On Windows, be aware that FreeType
will keep the file open as long as the FreeTypeFont object exists. Windows will keep the file open as long as the FreeTypeFont object exists. Windows
@ -927,7 +947,10 @@ def load_path(filename: str | bytes) -> ImageFont:
return load(os.path.join(directory, filename)) return load(os.path.join(directory, filename))
except OSError: except OSError:
pass pass
msg = "cannot find font file" msg = f'cannot find font file "{filename}" in sys.path'
if os.path.exists(filename):
msg += f', did you mean ImageFont.load("{filename}") instead?'
raise OSError(msg) raise OSError(msg)

View File

@ -265,7 +265,7 @@ def lambda_eval(
args.update(options) args.update(options)
args.update(kw) args.update(kw)
for k, v in args.items(): for k, v in args.items():
if hasattr(v, "im"): if isinstance(v, Image.Image):
args[k] = _Operand(v) args[k] = _Operand(v)
out = expression(args) out = expression(args)
@ -316,7 +316,7 @@ def unsafe_eval(
args.update(options) args.update(options)
args.update(kw) args.update(kw)
for k, v in args.items(): for k, v in args.items():
if hasattr(v, "im"): if isinstance(v, Image.Image):
args[k] = _Operand(v) args[k] = _Operand(v)
compiled_code = compile(expression, "<string>", "eval") compiled_code = compile(expression, "<string>", "eval")

View File

@ -98,14 +98,15 @@ class Dib:
HDC or HWND instance. In PythonWin, you can use HDC or HWND instance. In PythonWin, you can use
``CDC.GetHandleAttrib()`` to get a suitable handle. ``CDC.GetHandleAttrib()`` to get a suitable handle.
""" """
handle_int = int(handle)
if isinstance(handle, HWND): if isinstance(handle, HWND):
dc = self.image.getdc(handle) dc = self.image.getdc(handle_int)
try: try:
self.image.expose(dc) self.image.expose(dc)
finally: finally:
self.image.releasedc(handle, dc) self.image.releasedc(handle_int, dc)
else: else:
self.image.expose(handle) self.image.expose(handle_int)
def draw( def draw(
self, self,
@ -124,14 +125,15 @@ class Dib:
""" """
if src is None: if src is None:
src = (0, 0) + self.size src = (0, 0) + self.size
handle_int = int(handle)
if isinstance(handle, HWND): if isinstance(handle, HWND):
dc = self.image.getdc(handle) dc = self.image.getdc(handle_int)
try: try:
self.image.draw(dc, dst, src) self.image.draw(dc, dst, src)
finally: finally:
self.image.releasedc(handle, dc) self.image.releasedc(handle_int, dc)
else: else:
self.image.draw(handle, dst, src) self.image.draw(handle_int, dst, src)
def query_palette(self, handle: int | HDC | HWND) -> int: def query_palette(self, handle: int | HDC | HWND) -> int:
""" """
@ -148,14 +150,15 @@ class Dib:
:return: The number of entries that were changed (if one or more entries, :return: The number of entries that were changed (if one or more entries,
this indicates that the image should be redrawn). this indicates that the image should be redrawn).
""" """
handle_int = int(handle)
if isinstance(handle, HWND): if isinstance(handle, HWND):
handle = self.image.getdc(handle) handle = self.image.getdc(handle_int)
try: try:
result = self.image.query_palette(handle) result = self.image.query_palette(handle)
finally: finally:
self.image.releasedc(handle, handle) self.image.releasedc(handle, handle)
else: else:
result = self.image.query_palette(handle) result = self.image.query_palette(handle_int)
return result return result
def paste( def paste(

View File

@ -140,7 +140,7 @@ def _safe_zlib_decompress(s: bytes) -> bytes:
dobj = zlib.decompressobj() dobj = zlib.decompressobj()
plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
if dobj.unconsumed_tail: if dobj.unconsumed_tail:
msg = "Decompressed Data Too Large" msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK"
raise ValueError(msg) raise ValueError(msg)
return plaintext return plaintext

View File

@ -1194,8 +1194,8 @@ class TiffImageFile(ImageFile.ImageFile):
# Create a new core image object on second and # Create a new core image object on second and
# subsequent frames in the image. Image may be # subsequent frames in the image. Image may be
# different size/mode. # different size/mode.
Image._decompression_bomb_check(self.size) Image._decompression_bomb_check(self._tile_size)
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self._tile_size)
def _seek(self, frame: int) -> None: def _seek(self, frame: int) -> None:
self.fp = self._fp self.fp = self._fp
@ -1275,6 +1275,11 @@ class TiffImageFile(ImageFile.ImageFile):
return self._load_libtiff() return self._load_libtiff()
return super().load() return super().load()
def load_prepare(self) -> None:
if self._im is None:
self.im = Image.core.new(self.mode, self._tile_size)
ImageFile.ImageFile.load_prepare(self)
def load_end(self) -> None: def load_end(self) -> None:
# allow closing if we're on the first frame, there's no next # allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below. # This is the ImageFile.load path only, libtiff specific below.
@ -1416,7 +1421,12 @@ class TiffImageFile(ImageFile.ImageFile):
if not isinstance(xsize, int) or not isinstance(ysize, int): if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions" msg = "Invalid dimensions"
raise ValueError(msg) raise ValueError(msg)
self._size = xsize, ysize self._tile_size = xsize, ysize
orientation = self.tag_v2.get(ExifTags.Base.Orientation)
if orientation in (5, 6, 7, 8):
self._size = ysize, xsize
else:
self._size = xsize, ysize
logger.debug("- size: %s", self.size) logger.debug("- size: %s", self.size)
@ -1559,7 +1569,7 @@ class TiffImageFile(ImageFile.ImageFile):
if STRIPOFFSETS in self.tag_v2: if STRIPOFFSETS in self.tag_v2:
offsets = self.tag_v2[STRIPOFFSETS] offsets = self.tag_v2[STRIPOFFSETS]
h = self.tag_v2.get(ROWSPERSTRIP, ysize) h = self.tag_v2.get(ROWSPERSTRIP, ysize)
w = self.size[0] w = xsize
else: else:
# tiled image # tiled image
offsets = self.tag_v2[TILEOFFSETS] offsets = self.tag_v2[TILEOFFSETS]
@ -1593,9 +1603,9 @@ class TiffImageFile(ImageFile.ImageFile):
) )
) )
x = x + w x = x + w
if x >= self.size[0]: if x >= xsize:
x, y = 0, y + h x, y = 0, y + h
if y >= self.size[1]: if y >= ysize:
x = y = 0 x = y = 0
layer += 1 layer += 1
else: else:

View File

@ -832,7 +832,7 @@ font_render(FontObject *self, PyObject *args) {
Imaging im; Imaging im;
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
int color = 0; /* is FT_LOAD_COLOR enabled? */ int color = 0; /* is FT_LOAD_COLOR enabled? */
int stroke_width = 0; float stroke_width = 0;
PY_LONG_LONG foreground_ink_long = 0; PY_LONG_LONG foreground_ink_long = 0;
unsigned int foreground_ink; unsigned int foreground_ink;
const char *mode = NULL; const char *mode = NULL;
@ -852,7 +852,7 @@ font_render(FontObject *self, PyObject *args) {
if (!PyArg_ParseTuple( if (!PyArg_ParseTuple(
args, args,
"OO|zzOzizLffO:render", "OO|zzOzfzLffO:render",
&string, &string,
&fill, &fill,
&mode, &mode,
@ -918,8 +918,8 @@ font_render(FontObject *self, PyObject *args) {
return NULL; return NULL;
} }
width += stroke_width * 2 + ceil(x_start); width += ceil(stroke_width * 2 + x_start);
height += stroke_width * 2 + ceil(y_start); height += ceil(stroke_width * 2 + y_start);
image = PyObject_CallFunction(fill, "ii", width, height); image = PyObject_CallFunction(fill, "ii", width, height);
if (image == Py_None) { if (image == Py_None) {
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
@ -932,8 +932,8 @@ font_render(FontObject *self, PyObject *args) {
im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC); im = (Imaging)PyCapsule_GetPointer(imagePtr, IMAGING_MAGIC);
Py_XDECREF(imagePtr); Py_XDECREF(imagePtr);
x_offset -= stroke_width; x_offset = round(x_offset - stroke_width);
y_offset -= stroke_width; y_offset = round(y_offset - stroke_width);
if (count == 0 || width == 0 || height == 0) { if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info); PyMem_Del(glyph_info);
return Py_BuildValue("N(ii)", image, x_offset, y_offset); return Py_BuildValue("N(ii)", image, x_offset, y_offset);
@ -948,7 +948,7 @@ font_render(FontObject *self, PyObject *args) {
FT_Stroker_Set( FT_Stroker_Set(
stroker, stroker,
(FT_Fixed)stroke_width * 64, (FT_Fixed)round(stroke_width * 64),
FT_STROKER_LINECAP_ROUND, FT_STROKER_LINECAP_ROUND,
FT_STROKER_LINEJOIN_ROUND, FT_STROKER_LINEJOIN_ROUND,
0 0
@ -986,8 +986,8 @@ font_render(FontObject *self, PyObject *args) {
} }
/* set pen position to text origin */ /* set pen position to text origin */
x = (-x_min + stroke_width + x_start) * 64; x = round((-x_min + stroke_width + x_start) * 64);
y = (-y_max + (-stroke_width) - y_start) * 64; y = round((-y_max + (-stroke_width) - y_start) * 64);
if (stroker == NULL) { if (stroker == NULL) {
load_flags |= FT_LOAD_RENDER; load_flags |= FT_LOAD_RENDER;

View File

@ -1411,10 +1411,3 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
} }
#endif #endif
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -30,26 +30,57 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
bbox[1] = -1; bbox[1] = -1;
bbox[2] = bbox[3] = 0; bbox[2] = bbox[3] = 0;
#define GETBBOX(image, mask) \ #define GETBBOX(image, mask) \
for (y = 0; y < im->ysize; y++) { \ /* first stage: looking for any pixels from top */ \
has_data = 0; \ for (y = 0; y < im->ysize; y++) { \
for (x = 0; x < im->xsize; x++) { \ has_data = 0; \
if (im->image[y][x] & mask) { \ for (x = 0; x < im->xsize; x++) { \
has_data = 1; \ if (im->image[y][x] & mask) { \
if (x < bbox[0]) { \ has_data = 1; \
bbox[0] = x; \ bbox[0] = x; \
} \ bbox[1] = y; \
if (x >= bbox[2]) { \ break; \
bbox[2] = x + 1; \ } \
} \ } \
} \ if (has_data) { \
} \ break; \
if (has_data) { \ } \
if (bbox[1] < 0) { \ } \
bbox[1] = y; \ /* Check that we have a box */ \
} \ if (bbox[1] < 0) { \
bbox[3] = y + 1; \ return 0; /* no data */ \
} \ } \
/* second stage: looking for any pixels from bottom */ \
for (y = im->ysize - 1; y >= bbox[1]; y--) { \
has_data = 0; \
for (x = 0; x < im->xsize; x++) { \
if (im->image[y][x] & mask) { \
has_data = 1; \
if (x < bbox[0]) { \
bbox[0] = x; \
} \
bbox[3] = y + 1; \
break; \
} \
} \
if (has_data) { \
break; \
} \
} \
/* third stage: looking for left and right boundaries */ \
for (y = bbox[1]; y < bbox[3]; y++) { \
for (x = 0; x < bbox[0]; x++) { \
if (im->image[y][x] & mask) { \
bbox[0] = x; \
break; \
} \
} \
for (x = im->xsize - 1; x >= bbox[2]; x--) { \
if (im->image[y][x] & mask) { \
bbox[2] = x + 1; \
break; \
} \
} \
} }
if (im->image8) { if (im->image8) {
@ -71,11 +102,6 @@ ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) {
GETBBOX(image32, mask); GETBBOX(image32, mask);
} }
/* Check that we got a box */
if (bbox[1] < 0) {
return 0; /* no data */
}
return 1; /* ok */ return 1; /* ok */
} }
@ -144,6 +170,9 @@ ImagingGetExtrema(Imaging im, void *extrema) {
imax = in[x]; imax = in[x];
} }
} }
if (imin == 0 && imax == 255) {
break;
}
} }
((UINT8 *)extrema)[0] = (UINT8)imin; ((UINT8 *)extrema)[0] = (UINT8)imin;
((UINT8 *)extrema)[1] = (UINT8)imax; ((UINT8 *)extrema)[1] = (UINT8)imax;

View File

@ -104,10 +104,3 @@ typedef struct {
int plt; int plt;
} JPEG2KENCODESTATE; } JPEG2KENCODESTATE;
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -979,10 +979,3 @@ ImagingJpeg2KVersion(void) {
} }
#endif /* HAVE_OPENJPEG */ #endif /* HAVE_OPENJPEG */
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -652,10 +652,3 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) {
} }
#endif /* HAVE_OPENJPEG */ #endif /* HAVE_OPENJPEG */
/*
* Local Variables:
* c-basic-offset: 4
* End:
*
*/

View File

@ -44,7 +44,6 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view);
typedef struct { typedef struct {
PyObject_HEAD Py_ssize_t count; PyObject_HEAD Py_ssize_t count;
double *xy; double *xy;
int index; /* temporary use, e.g. in decimate */
} PyPathObject; } PyPathObject;
static PyTypeObject PyPathType; static PyTypeObject PyPathType;

View File

@ -1,11 +1,11 @@
README README
------ ------
[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux [cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build wheels for tagged
wheels for tagged versions of Pillow. versions of Pillow.
This directory contains [multibuild](https://github.com/multi-build/multibuild) to This directory contains [multibuild](https://github.com/multi-build/multibuild) to
build dependencies for the wheels, and dependency licenses to be included. build dependencies for macOS and Linux wheels, and dependency licenses to be included.
Archives Archives
-------- --------
@ -30,6 +30,3 @@ Wheels
Wheels are Wheels are
[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). [GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml).
Windows wheels are created separately. They are
[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain).

View File

@ -113,13 +113,13 @@ V = {
"FREETYPE": "2.13.3", "FREETYPE": "2.13.3",
"FRIBIDI": "1.0.15", "FRIBIDI": "1.0.15",
"HARFBUZZ": "9.0.0", "HARFBUZZ": "9.0.0",
"JPEGTURBO": "3.0.3", "JPEGTURBO": "3.0.4",
"LCMS2": "2.16", "LCMS2": "2.16",
"LIBPNG": "1.6.43", "LIBPNG": "1.6.44",
"LIBWEBP": "1.4.0", "LIBWEBP": "1.4.0",
"OPENJPEG": "2.5.2", "OPENJPEG": "2.5.2",
"TIFF": "4.6.0", "TIFF": "4.6.0",
"XZ": "5.4.5", "XZ": "5.6.2",
"ZLIB": "1.3.1", "ZLIB": "1.3.1",
} }
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "") V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
@ -175,7 +175,7 @@ DEPS: dict[str, dict[str, Any]] = {
"libs": [r"*.lib"], "libs": [r"*.lib"],
}, },
"xz": { "xz": {
"url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download", "url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz",
"filename": f"xz-{V['XZ']}.tar.gz", "filename": f"xz-{V['XZ']}.tar.gz",
"dir": f"xz-{V['XZ']}", "dir": f"xz-{V['XZ']}",
"license": "COPYING", "license": "COPYING",