Merge branch 'main' into bitmap_buffer

This commit is contained in:
Andrew Murray 2024-09-30 19:47:11 +10:00 committed by GitHub
commit 94c3ee6944
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
133 changed files with 1751 additions and 930 deletions

View File

@ -34,8 +34,8 @@ install:
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.3.1
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- choco install ghostscript --version=10.4.0
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
@ -51,11 +51,10 @@ build_script:
test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest?
- path %PYTHON%;%PATH%
- .ci\test.cmd
after_test:
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe

View File

@ -21,7 +21,7 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then
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\
sway wl-clipboard libopenblas-dev
fi
@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
@ -37,12 +38,7 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports free-threading
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
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@ -52,10 +48,7 @@ if [[ $(uname) != CYGWIN* ]]; then
fi
# Pyroma uses non-isolated build and fails with old setuptools
if [[
$GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.9
]]; then
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=67.8"
fi

View File

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

View File

@ -1,4 +1,4 @@
mypy==1.11.1
mypy==1.11.2
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
@ -6,6 +6,7 @@ numpy
packaging
pytest
sphinx
types-atheris
types-defusedxml
types-olefile
types-setuptools

3
.ci/test.cmd Normal file
View File

@ -0,0 +1,3 @@
python.exe -c "from PIL import Image"
IF ERRORLEVEL 1 EXIT /B
python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests

View File

@ -4,4 +4,4 @@ set -e
python3 -c "from PIL import Image"
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE

View File

@ -24,8 +24,6 @@ concurrency:
jobs:
Fuzzing:
# Disabled until google/oss-fuzz#11419 upgrades Python to 3.9+
if: false
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers

View File

@ -23,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov

View File

@ -74,6 +74,7 @@ jobs:
perl
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter

View File

@ -80,8 +80,7 @@ jobs:
- name: Test Pillow
run: |
python3 selftest.py --installed
python3 -c "from PIL import Image"
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
.ci/test.sh
- name: Upload coverage
uses: codecov/codecov-action@v4

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "pypy3.9", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
@ -86,8 +86,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.4.0 --no-progress
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
@ -190,8 +190,8 @@ jobs:
- name: Test Pillow
run: |
path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH%
python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd
shell: cmd
- name: Prepare to upload errors

View File

@ -37,12 +37,11 @@ jobs:
fail-fast: false
matrix:
os: [
"macos-14",
"macos-latest",
"ubuntu-latest",
]
python-version: [
"pypy3.10",
"pypy3.9",
"3.13",
"3.12",
"3.11",
@ -57,7 +56,7 @@ jobs:
# M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" }
exclude:
- { os: "macos-14", python-version: "3.9" }
- { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
@ -77,7 +76,7 @@ jobs:
"pyproject.toml"
- 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 }}"
with:
python-version: ${{ matrix.python-version }}

View File

@ -16,11 +16,11 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.5.0
LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.3
HARFBUZZ_VERSION=10.0.1
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.0.4
OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5
XZ_VERSION=5.6.2
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
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 \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
@ -50,7 +50,7 @@ fi
function build_brotli {
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 \
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
@ -60,6 +60,19 @@ function build_brotli {
fi
}
function build_harfbuzz {
python3 -m pip install meson ninja
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
(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
}
function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
sudo chown -R runner /usr/local
@ -109,15 +122,7 @@ function build {
build_freetype
fi
if [ -z "$IS_MACOS" ]; then
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
build_harfbuzz
}
# Any stuff that you need to do before you start building the wheels

View File

@ -13,14 +13,7 @@ else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
# 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
python3 -m pip install numpy
if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip

View File

@ -48,7 +48,6 @@ jobs:
fail-fast: false
matrix:
python-version:
- pp39
- pp310
- cp3{9,10,11}
- cp3{12,13}
@ -57,7 +56,6 @@ jobs:
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp39, spec: musllinux }
- { python-version: pp310, spec: musllinux }
steps:
@ -104,12 +102,23 @@ jobs:
fail-fast: false
matrix:
include:
- name: "macOS x86_64"
- name: "macOS 10.10 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{9,10,11}*"
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 10.15 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
os: macos-14
os: macos-latest
cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
@ -147,7 +156,7 @@ jobs:
- uses: actions/upload-artifact@v4
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
windows:
@ -269,7 +278,7 @@ jobs:
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0
uses: scientific-python/upload-nightly-action@ccf29c805b5d0c1dc31fa97fcdb962be074cade3 # 0.6.0
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.0
rev: v0.6.3
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
@ -50,7 +50,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.1
rev: 0.29.2
hooks:
- id: check-github-workflows
- id: check-readthedocs

View File

@ -5,6 +5,87 @@ Changelog (Pillow)
11.0.0 (unreleased)
-------------------
- Improved copying imagequant libraries #8420
[radarhere]
- Use Capsule for WebP saving #8386
[homm, radarhere]
- Fixed writing multiple StripOffsets to TIFF #8317
[Yay295, radarhere]
- Fix dereference before checking for NULL in ImagingTransformAffine #8398
[PavlNekrasov]
- Use transposed size after opening for TIFF images #8390
[radarhere, homm]
- Improve ImageFont error messages #8338
[yngvem, radarhere, hugovk]
- Mention MAX_TEXT_CHUNK limit in PNG error message #8391
[radarhere]
- Cast Dib handle to int #8385
[radarhere]
- 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
[radarhere]
- Handle duplicate EXIF header #8350
[zakajd, radarhere]
- Return early from BoxBlur if either width or height is zero #8347
[radarhere]
- Check text is either string or bytes #8308
[radarhere]
- Added writing XMP bytes to JPEG #8286
[radarhere]
- Support JPEG2000 RGBA palettes #8256
[radarhere]
- Expand C image to match GIF frame image size #8237
[radarhere]
- Allow saving I;16 images as PPM #8231
[radarhere]
- When IFD is missing, connect get_ifd() dictionary to Exif #8230
[radarhere]
- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180
[radarhere]
- Treat unknown JPEG2000 colorspace as unspecified #8343
[radarhere]
- Updated error message when saving WebP with invalid width or height #8322
[radarhere, hugovk]
- Remove warning if NumPy failed to raise an error during conversion #8326
[radarhere]
- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304
[radarhere]

View File

@ -117,7 +117,7 @@ lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix .
python3 -m ruff check --fix .
.PHONY: mypy
mypy:

View File

@ -51,7 +51,7 @@ As of 2019, Pillow development is
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
<a href="https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow"><img
<a href="https://issues.oss-fuzz.com/issues?q=title:pillow"><img
alt="Fuzzing Status"
src="https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg"></a>
</td>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

View File

@ -16,8 +16,9 @@
import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports():
with instrument_imports():
import sys
import fuzzers

View File

@ -14,8 +14,9 @@
import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports():
with instrument_imports():
import sys
import fuzzers

View File

@ -1,5 +1,5 @@
{
<py3_8_encode_current_locale>
<py3_10_encode_current_locale>
Memcheck:Cond
...
fun:encode_current_locale

View File

@ -71,6 +71,11 @@ def test_color_modes() -> None:
box_blur(sample.convert("YCbCr"))
@pytest.mark.parametrize("size", ((0, 1), (1, 0)))
def test_zero_dimension(size: tuple[int, int]) -> None:
assert box_blur(Image.new("L", size)).size == size
def test_radius_0() -> None:
assert_blur(
sample,

View File

@ -120,7 +120,6 @@ class TestColorLut3DCoreAPI:
self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int]
) -> None:
im = Image.new("RGB", (10, 10), 0)
assert im.im is not None
im.im.color_lut_3d(
lut_mode,
Image.Resampling.BILINEAR,
@ -142,7 +141,6 @@ class TestColorLut3DCoreAPI:
) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new(image_mode, (10, 10), 0)
assert im.im is not None
im.im.color_lut_3d(
lut_mode,
Image.Resampling.BILINEAR,
@ -162,7 +160,6 @@ class TestColorLut3DCoreAPI:
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
im = Image.new(image_mode, (10, 10), 0)
assert im.im is not None
im.im.color_lut_3d(
lut_mode,
Image.Resampling.BILINEAR,

View File

@ -1378,8 +1378,26 @@ def test_lzw_bits() -> None:
im.load()
def test_extents() -> None:
with Image.open("Tests/images/test_extents.gif") as im:
@pytest.mark.parametrize(
"test_file, loading_strategy",
(
("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),
(
"test_extents.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
),
(
"test_extents_transparency.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
),
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
with Image.open("Tests/images/" + test_file) as im:
assert im.size == (100, 100)
# Check that n_frames does not change the size
@ -1389,6 +1407,11 @@ def test_extents() -> None:
im.seek(1)
assert im.size == (150, 150)
im.load()
assert im.im.size == (150, 150)
finally:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
def test_missing_background() -> None:
# The Global Color Table Flag isn't set, so there is no background color index,
@ -1406,3 +1429,21 @@ def test_saving_rgba(tmp_path: Path) -> None:
with Image.open(out) as reloaded:
reloaded_rgba = reloaded.convert("RGBA")
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)
with Image.open(temp_file) as reread:
reread.size = (16, 16, 2)
reread.load()
reread.size = (16, 16)
reread.load(2)
assert_image_equal(reread, provided_im)
@ -87,14 +87,21 @@ def test_sizes() -> None:
for w, h, r in im.info["sizes"]:
wr = w * r
hr = h * r
with pytest.warns(DeprecationWarning):
im.size = (w, h, r)
im.load()
assert im.mode == "RGBA"
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
with pytest.raises(ValueError):
im.size = (1, 1)
im.size = (1, 2)
def test_older_icon() -> None:
@ -105,8 +112,8 @@ def test_older_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2:
im2.size = (w, h, r)
im2.load()
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
assert im2.size == (wr, hr)
@ -122,8 +129,8 @@ def test_jp2_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2:
im2.size = (w, h, r)
im2.load()
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
assert im2.size == (wr, hr)

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest
from PIL import IcoImagePlugin, Image, ImageDraw
from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
@ -241,3 +241,29 @@ def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(outfile) as im:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_truncated_mask() -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()
ImageFile.LOAD_TRUNCATED_IMAGES = True
data = data[:-3]
try:
with Image.open(io.BytesIO(data)) as im:
with Image.open("Tests/images/hopper_mask.png") as expected:
assert im.mode == "1"
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
data = output.getvalue()[:-1]
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False

View File

@ -991,12 +991,29 @@ class TestFileJpeg:
else:
assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"
im.info["xmp"] = b"1" * 65504
im.save(f)
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504
with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1)
def test_eof(self) -> None:
# Even though this decoder never says that it is finished
# the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(
self, buffer: bytes | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0
Image.register_decoder("INFINITE", InfiniteMockPyDecoder)

View File

@ -182,6 +182,15 @@ def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_unknown_colorspace() -> None:
with Image.open(f"{EXTRA_DIR}/file8.jp2") as im:
im.load()
assert im.mode == "L"
def test_header_errors() -> None:
for path in (
"Tests/images/invalid_header_length.jp2",
@ -391,6 +400,13 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
assert len(im.palette.colors) == 139
assert im.palette.colors[(0, 0, 0, 0)] == 0
def test_comment() -> None:
with Image.open("Tests/images/comment.jp2") as im:

View File

@ -95,7 +95,9 @@ def test_16bit_pgm_write(tmp_path: Path) -> None:
with Image.open("Tests/images/16_bit_binary.pgm") as im:
filename = str(tmp_path / "temp.pgm")
im.save(filename, "PPM")
assert_image_equal_tofile(im, filename)
im.convert("I;16").save(filename, "PPM")
assert_image_equal_tofile(im, filename)

View File

@ -108,7 +108,8 @@ class TestFileTiff:
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
# multistrip support not yet implemented
# The data type of this file's StripOffsets tag is LONG8,
# which is not yet supported for offset data when saving multiple frames.
del im.tag_v2[273]
outfile = str(tmp_path / "temp.tif")
@ -684,6 +685,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)
def test_invalid_tiled_dimensions(self) -> None:
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
data = fp.read()
b = BytesIO(data[:144] + b"\x02" + data[145:])
with pytest.raises(ValueError):
Image.open(b)
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")

View File

@ -181,6 +181,29 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
def test_save_multiple_stripoffsets() -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[TiffImagePlugin.STRIPOFFSETS] = (123, 456)
assert ifd.tagtype[TiffImagePlugin.STRIPOFFSETS] == TiffTags.LONG
# all values are in little-endian
assert ifd.tobytes() == (
# number of tags == 1
b"\x01\x00"
# tag id (2 bytes), type (2 bytes), count (4 bytes), value (4 bytes)
# TiffImagePlugin.STRIPOFFSETS, TiffTags.LONG, 2, 18
# where STRIPOFFSETS is 273, LONG is 4
# and 18 is the offset of the tag data
b"\x11\x01\x04\x00\x02\x00\x00\x00\x12\x00\x00\x00"
# end of entries
b"\x00\x00\x00\x00"
# 26 is the total number of bytes output,
# the offset for any auxiliary strip data that will then be appended
# (123 + 26, 456 + 26) == (149, 482)
b"\x95\x00\x00\x00\xe2\x01\x00\x00"
)
def test_no_duplicate_50741_tag() -> None:
assert TAG_IDS["MakerNoteSafety"] == 50741
assert TAG_IDS["BestQualityScale"] == 50780

View File

@ -72,7 +72,7 @@ class TestFileWebp:
def _roundtrip(
self, tmp_path: Path, mode: str, epsilon: float, args: dict[str, Any] = {}
) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
hopper(mode).save(temp_file, **args)
with Image.open(temp_file) as image:
@ -116,7 +116,7 @@ class TestFileWebp:
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
def test_save_all(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im.save(temp_file, save_all=True, append_images=[im2])
@ -127,6 +127,11 @@ class TestFileWebp:
reloaded.seek(1)
assert_image_similar(im2, reloaded, 1)
def test_unsupported_image_mode(self) -> None:
im = Image.new("1", (1, 1))
with pytest.raises(ValueError):
_webp.WebPEncode(im.getim(), False, 0, 0, "", 4, 0, b"", "")
def test_icc_profile(self, tmp_path: Path) -> None:
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
self._roundtrip(
@ -151,12 +156,21 @@ class TestFileWebp:
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
im = Image.new("RGB", (15000, 15000))
with pytest.raises(ValueError) as e:
im.save(temp_file, method=0)
im.save(tmp_path / "temp.webp", method=0)
assert str(e.value) == "encoding error 6"
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
im = Image.new("L", (16384, 16384))
with pytest.raises(ValueError) as e:
im.save(tmp_path / "temp.webp")
assert (
str(e.value)
== "encoding error 5: Image size exceeds WebP limit of 16383 pixels"
)
def test_WebPEncode_with_invalid_args(self) -> None:
"""
Calling encoder functions with no arguments should result in an error.
@ -176,9 +190,8 @@ class TestFileWebp:
def test_no_resource_warning(self, tmp_path: Path) -> None:
file_path = "Tests/images/hopper.webp"
with Image.open(file_path) as image:
temp_file = str(tmp_path / "temp.webp")
with warnings.catch_warnings():
image.save(temp_file)
image.save(tmp_path / "temp.webp")
def test_file_pointer_could_be_reused(self) -> None:
file_path = "Tests/images/hopper.webp"
@ -193,15 +206,16 @@ class TestFileWebp:
def test_invalid_background(
self, background: int | tuple[int, ...], tmp_path: Path
) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = hopper()
with pytest.raises(OSError):
im.save(temp_file, save_all=True, append_images=[im], background=background)
def test_background_from_gif(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"
# Save L mode GIF with background
with Image.open("Tests/images/no_palette_with_background.gif") as im:
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
# Save P mode GIF with background
@ -209,11 +223,10 @@ class TestFileWebp:
original_value = im.convert("RGB").getpixel((1, 1))
# Save as WEBP
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
# Save as GIF
out_gif = str(tmp_path / "temp.gif")
out_gif = tmp_path / "temp.gif"
with Image.open(out_webp) as im:
im.save(out_gif)
@ -223,10 +236,10 @@ class TestFileWebp:
assert difference < 5
def test_duration(self, tmp_path: Path) -> None:
out_webp = tmp_path / "temp.webp"
with Image.open("Tests/images/dispose_bgnd.gif") as im:
assert im.info["duration"] == 1000
out_webp = str(tmp_path / "temp.webp")
im.save(out_webp, save_all=True)
with Image.open(out_webp) as reloaded:
@ -234,9 +247,10 @@ class TestFileWebp:
assert reloaded.info["duration"] == 1000
def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None:
temp_file = str(tmp_path / "temp.webp")
temp_file = tmp_path / "temp.webp"
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "RGBA"
im.save(temp_file)

View File

@ -116,7 +116,15 @@ def test_read_no_exif() -> None:
def test_getxmp() -> None:
with Image.open("Tests/images/flower.webp") as im:
assert "xmp" not in im.info
assert im.getxmp() == {}
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None:

View File

@ -42,6 +42,12 @@ try:
except ImportError:
ElementTree = None
PrettyPrinter: type | None
try:
from IPython.lib.pretty import PrettyPrinter
except ImportError:
PrettyPrinter = None
# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -91,16 +97,15 @@ class TestImage:
# with pytest.raises(MemoryError):
# Image.new("L", (1000000, 1000000))
@pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
def test_repr_pretty(self) -> None:
class Pretty:
def text(self, text: str) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100))
p = Pretty()
output = io.StringIO()
assert PrettyPrinter is not None
p = PrettyPrinter(output)
im._repr_pretty_(p, False)
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>"
assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"
def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png"
@ -700,6 +705,7 @@ class TestImage:
assert new_image.size == image.size
assert new_image.info == base_image.info
if palette_result is not None:
assert new_image.palette is not None
assert new_image.palette.tobytes() == palette_result.tobytes()
else:
assert new_image.palette is None
@ -769,6 +775,22 @@ class TestImage:
exif.load(b"Exif\x00\x00")
assert not dict(exif)
def test_duplicate_exif_header(self) -> None:
with Image.open("Tests/images/exif.png") as im:
im.load()
im.info["exif"] = b"Exif\x00\x00" + im.info["exif"]
exif = im.getexif()
assert exif[274] == 1
def test_empty_get_ifd(self) -> None:
exif = Image.Exif()
ifd = exif.get_ifd(0x8769)
assert ifd == {}
ifd[36864] = b"0220"
assert exif.get_ifd(0x8769) == {36864: b"0220"}
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
@ -938,7 +960,15 @@ class TestImage:
def test_empty_xmp(self) -> None:
with Image.open("Tests/images/hopper.gif") as im:
assert im.getxmp() == {}
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
xmp = im.getxmp()
else:
xmp = im.getxmp()
assert xmp == {}
def test_getxmp_padded(self) -> None:
im = Image.new("RGB", (1, 1))
@ -989,12 +1019,14 @@ class TestImage:
# P mode with RGBA palette
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette is not None
assert im.palette.mode == "RGBA"
assert im.has_transparency_data
def test_apply_transparency(self) -> None:
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1))
assert im.palette is not None
assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
# Test that no transformation is applied without transparency
@ -1012,13 +1044,16 @@ class TestImage:
im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
im.info["transparency"] = 0
im.apply_transparency()
assert im.palette is not None
assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1}
# Test that transparency bytes are applied
with Image.open("Tests/images/pil123p.png") as im:
assert isinstance(im.info["transparency"], bytes)
assert im.palette is not None
assert im.palette.colors[(27, 35, 6)] == 24
im.apply_transparency()
assert im.palette is not None
assert im.palette.colors[(27, 35, 6, 214)] == 24
def test_constants(self) -> None:
@ -1051,22 +1086,17 @@ class TestImage:
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
"""
with Image.open(os.path.join("Tests/images", path)) as im:
try:
with pytest.raises(OSError) as e:
im.load()
pytest.fail()
except OSError as e:
buffer_overrun = str(e) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e)
buffer_overrun = str(e.value) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e.value)
assert buffer_overrun or truncated
def test_fli_overrun2(self) -> None:
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)
pytest.fail()
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
def test_exit_fp(self) -> None:
with Image.new("L", (1, 1)) as im:
@ -1082,6 +1112,10 @@ class TestImage:
assert len(caplog.records) == 0
assert im.fp is None
def test_deprecation(self) -> None:
with pytest.warns(DeprecationWarning):
assert not Image.isImageType(None)
class TestImageBytes:
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])

View File

@ -24,7 +24,7 @@ def test_toarray() -> None:
def test_with_dtype(dtype: npt.DTypeLike) -> None:
ai = numpy.array(im, dtype=dtype)
assert ai.dtype == dtype
assert ai.dtype.type is dtype
# assert test("1") == ((100, 128), '|b1', 1600))
assert test("L") == ((100, 128), "|u1", 12800)
@ -47,7 +47,7 @@ def test_toarray() -> None:
with pytest.raises(OSError):
numpy.array(im_truncated)
else:
with pytest.warns(UserWarning):
with pytest.warns(DeprecationWarning):
numpy.array(im_truncated)
@ -113,4 +113,5 @@ def test_fromarray_palette() -> None:
out = Image.fromarray(a, "P")
# Assert that the Python and C palettes match
assert out.palette is not None
assert len(out.palette.colors) == len(out.im.getpalette()) / 3

View File

@ -218,6 +218,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
def test_l_macro_rounding(convert_mode: str) -> None:
for mode in ("P", "PA"):
im = Image.new(mode, (1, 1))
assert im.palette is not None
im.palette.getcolor((0, 1, 2))
converted_im = im.convert(convert_mode)

View File

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

View File

@ -86,6 +86,7 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
im = Image.new("P", (1, 1))
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]
assert im.palette is not None
assert im.palette.colors == {(1, 2, 3, 4): 0}

View File

@ -69,6 +69,7 @@ def test_quantize_no_dither() -> None:
converted = image.quantize(dither=Image.Dither.NONE, palette=palette)
assert converted.mode == "P"
assert converted.palette is not None
assert converted.palette.palette == palette.palette.palette
@ -81,6 +82,7 @@ def test_quantize_no_dither2() -> None:
palette.putpalette(data)
quantized = im.quantize(dither=Image.Dither.NONE, palette=palette)
assert quantized.palette is not None
assert tuple(quantized.palette.palette) == data
px = quantized.load()
@ -117,6 +119,7 @@ def test_colors() -> None:
im = hopper()
colors = 2
converted = im.quantize(colors)
assert converted.palette is not None
assert len(converted.palette.palette) == colors * len("RGB")
@ -147,6 +150,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method)
converted_px = converted.load()
assert converted_px is not None
assert converted.palette is not None
assert converted_px[0, 0] == converted.palette.colors[color]

View File

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

View File

@ -92,15 +92,13 @@ def test_no_resize() -> None:
@skip_unless_feature("libtiff")
def test_load_first() -> None:
# load() may change the size of the image
# Test that thumbnail() is calling it before performing size calculations
def test_transposed() -> None:
with Image.open("Tests/images/g4_orientation_5.tif") as im:
assert im.size == (590, 88)
im.thumbnail((64, 64))
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:
im.thumbnail((590, 88), reducing_gap=None)
assert im.size == (590, 88)

View File

@ -696,6 +696,12 @@ def test_rgb_lab(mode: str) -> None:
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:
with pytest.warns(DeprecationWarning):
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")
def test_stroke_descender() -> None:
# Arrange

View File

@ -210,7 +210,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args)
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
# eof
return -1, 0
@ -238,7 +238,9 @@ class MockImageFile(ImageFile.ImageFile):
self.rawmode = "RGBA"
self._mode = "RGBA"
self._size = (200, 200)
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
self.tile = [
ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)
]
class CodecsTest:
@ -268,7 +270,7 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
im.load()
@ -281,12 +283,12 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
with pytest.raises(ValueError):
im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
with pytest.raises(ValueError):
im.load()
@ -294,12 +296,20 @@ class TestPyDecoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None
)
]
with pytest.raises(ValueError):
im.load()
im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)]
im.tile = [
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None
)
]
with pytest.raises(ValueError):
im.load()
@ -336,7 +346,7 @@ class TestPyEncoder(CodecsTest):
buf = BytesIO(b"\x00" * 255)
im = MockImageFile(buf)
im.tile = [("MOCK", None, 32, None)]
im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
fp = BytesIO()
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])
@ -412,9 +422,8 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()
fh = BytesIO()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(fh, 0)
encoder.encode_to_file(0, 0)
def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError):

View File

@ -5,6 +5,7 @@ import os
import re
import shutil
import sys
import tempfile
from io import BytesIO
from pathlib import Path
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)
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:
# Arrange
filename = "somefilenamethatdoesntexist.ttf"
# Act/Assert
with pytest.raises(OSError):
with pytest.raises(OSError) as e:
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):
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:
with open("Tests/images/hopper.jpg", "rb") as f:
with pytest.raises(OSError):
@ -1113,6 +1140,9 @@ def test_bytes(font: ImageFont.FreeTypeFont) -> None:
)
assert font.getmask2(b"test")[1] == font.getmask2("test")[1]
with pytest.raises(TypeError):
font.getlength((0, 0)) # type: ignore[arg-type]
@pytest.mark.parametrize(
"test_file",
@ -1147,3 +1177,15 @@ def test_invalid_truetype_sizes_raise_valueerror(
) -> None:
with pytest.raises(ValueError):
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
break
else:
pytest.fail()
pytest.fail("IPythonViewer not found")
im = hopper()
assert test_viewer.show(im) == 1

View File

@ -60,6 +60,18 @@ class TestImageWinDib:
with pytest.raises(ValueError):
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:
# Arrange
im = hopper()

View File

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

View File

@ -5,8 +5,6 @@ import sys
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, PSDraw
@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None:
assert os.path.getsize(tempfile) > 0
@pytest.mark.parametrize("buffer", (True, False))
def test_stdout(buffer: bool) -> None:
def test_stdout() -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout
class MyStdOut:
buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
mystdout = MyStdOut()
sys.stdout = mystdout
@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""
assert mystdout.buffer.getvalue() != b""

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Union
import pytest
@ -8,6 +9,20 @@ from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
if TYPE_CHECKING:
import PyQt6
import PySide6
QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@ -20,7 +35,7 @@ if ImageQt.qt_is_installed:
from PySide6.QtGui import QImage, QPainter, QRegion
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
class Example(QWidget):
class Example(QWidget): # type: ignore[misc]
def __init__(self) -> None:
super().__init__()
@ -28,11 +43,12 @@ if ImageQt.qt_is_installed:
qimage = ImageQt.ImageQt(img)
pixmap1 = ImageQt.QPixmap.fromImage(qimage)
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
QHBoxLayout(self) # hbox
# hbox
QHBoxLayout(self) # type: ignore[operator]
lbl = QLabel(self)
lbl = QLabel(self) # type: ignore[operator]
# Segfault in the problem
lbl.setPixmap(pixmap1.copy())
@ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None:
# Segfault test
app: QApplication | None = QApplication([])
app: QApplication | None = QApplication([]) # type: ignore[operator]
ex = Example()
assert app # Silence warning
assert ex # Silence warning
@ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None:
im = hopper(mode)
data = ImageQt.toqpixmap(im)
assert isinstance(data, QPixmap)
assert data.__class__.__name__ == "QPixmap"
assert not data.isNull()
# Test saving the file
@ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None:
data.save(tempfile)
# Render the image
qimage = ImageQt.ImageQt(im)
data = QPixmap.fromImage(qimage)
qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, qt_format.Format_ARGB32)
painter = QPainter(qimage)
image_label = QLabel()
imageqt = ImageQt.ImageQt(im)
data = getattr(QPixmap, "fromImage")(imageqt)
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
painter = QPainter(qimage) # type: ignore[operator]
image_label = QLabel() # type: ignore[operator]
image_label.setPixmap(data)
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
painter.end()
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
qimage.save(rendered_tempfile)

View File

@ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
assert isinstance(data, QImage)
assert isinstance(data, QImage) # type: ignore[arg-type, misc]
assert not data.isNull()
# reload directly from the qimage

View File

@ -2,7 +2,7 @@
# install libimagequant
archive_name=libimagequant
archive_version=4.3.1
archive_version=4.3.3
archive=$archive_name-$archive_version
@ -23,14 +23,14 @@ else
cargo cinstall --prefix=/usr --destdir=.
# Copy into place
sudo cp usr/lib/libimagequant.so* /usr/lib/
sudo find usr -name libimagequant.so* -exec cp {} /usr/lib/ \;
sudo cp usr/include/libimagequant.h /usr/include/
if [ -n "$GITHUB_ACTIONS" ]; then
# Copy to cache
rm -rf ~/cache-$archive_name
mkdir ~/cache-$archive_name
cp usr/lib/libimagequant.so* ~/cache-$archive_name/
find usr -name libimagequant.so* -exec cp {} ~/cache-$archive_name/ \;
cp usr/include/libimagequant.h ~/cache-$archive_name/
fi

View File

@ -2,7 +2,7 @@
# install raqm
archive=libraqm-0.10.1
archive=libraqm-0.10.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -121,7 +121,7 @@ nitpicky = True
# generating warnings in “nitpicky mode”. Note that type should include the domain name
# if present. Example entries would be ('py:func', 'int') or
# ('envvar', 'LD_LIBRARY_PATH').
# nitpick_ignore = []
nitpick_ignore = [("py:class", "_io.BytesIO")]
# -- Options for HTML output ----------------------------------------------

View File

@ -109,6 +109,35 @@ ImageDraw.getdraw hints parameter
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -142,6 +171,13 @@ Removed features
Deprecated features are only removed in major releases after an appropriate
period of deprecation has passed.
TiffImagePlugin IFD_LEGACY_API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 11.0.0
``TiffImagePlugin.IFD_LEGACY_API`` was removed, as it was an unused setting.
PSFile
~~~~~~

View File

@ -246,7 +246,9 @@ class DdsImageFile(ImageFile.ImageFile):
msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg)
self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
self.tile = [
ImageFile._Tile(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
]
def load_seek(self, pos: int) -> None:
pass
@ -255,7 +257,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
@ -268,7 +270,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))

View File

@ -324,12 +324,19 @@ sets the following :py:attr:`~PIL.Image.Image.info` property:
**sizes**
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
icon and 1 for a standard icon. You *are* permitted to use this 3-tuple
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
will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you
ask for ``(512, 512, 2)``, the final value of
:py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``).
icon and 1 for a standard icon.
.. _icns-loading:
Loading
~~~~~~~
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:

View File

@ -54,7 +54,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:alt: Tidelift
.. image:: https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg
:target: https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow
:target: https://issues.oss-fuzz.com/issues?q=title:pillow
:alt: Fuzzing Status
.. image:: https://img.shields.io/pypi/v/pillow.svg

View File

@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.3.1**
* Pillow has been tested with libimagequant **2.6-4.3.3**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.

View File

@ -362,6 +362,7 @@ Classes
:undoc-members:
:show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImagePointTransform
.. autoclass:: PIL.Image.ImageTransformHandler
Protocols

View File

@ -33,6 +33,14 @@ Internal Modules
Provides a convenient way to import type hints that are not available
on some Python versions.
.. py:class:: Buffer
Typing alias.
.. py:class:: IntegralLike
Typing alias.
.. py:class:: NumpyArray
Typing alias.

View File

@ -23,6 +23,13 @@ Python 3.8
Pillow has dropped support for Python 3.8,
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
^^^^^^
@ -40,12 +47,53 @@ removed. Pillow's C API will now be used on PyPy instead.
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, was
similarly removed.
TiffImagePlugin IFD_LEGACY_API
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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
============
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more
keyword arguments can be used instead.
@ -61,6 +109,8 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15).
Specific WebP Feature Checks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 11.0.0
``features.check("transp_webp")``, ``features.check("webp_mux")`` and
``features.check("webp_anim")`` are now deprecated. They will always return
``True`` if the WebP module is installed, until they are removed in Pillow
@ -77,10 +127,18 @@ TODO
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
=============
@ -94,6 +152,10 @@ of 3.13.0 final (2024-10-01, :pep:`719`).
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
^^^^^^^^^^^^^

View File

@ -97,9 +97,13 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests"
[tool.ruff]
fix = true
[tool.black]
exclude = "wheels/multibuild"
[tool.ruff]
exclude = [ "wheels/multibuild" ]
fix = true
lint.select = [
"C4", # flake8-comprehensions
"E", # pycodestyle errors
@ -125,7 +129,6 @@ lint.ignore = [
"PT007", # pytest-parametrize-values-wrong-type
"PT011", # pytest-raises-too-broad
"PT012", # pytest-raises-with-multiple-statements
"PT016", # pytest-fail-without-message
"PT017", # pytest-assert-in-except
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
@ -163,8 +166,3 @@ follow_imports = "silent"
warn_redundant_casts = true
warn_unreachable = true
warn_unused_ignores = true
exclude = [
'^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$',
'^Tests/test_qt_image_qapplication.py$',
]

View File

@ -52,7 +52,7 @@ def testimage() -> None:
or you call the "load" method:
>>> im = Image.open("Tests/images/hopper.ppm")
>>> print(im.im) # internal image attribute
>>> print(im._im) # internal image attribute
None
>>> a = im.load()
>>> type(im.im) # doctest: +ELLIPSIS

249
setup.py
View File

@ -15,18 +15,20 @@ import struct
import subprocess
import sys
import warnings
from collections.abc import Iterator
from typing import Any
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
def get_version():
def get_version() -> str:
version_file = "src/PIL/_version.py"
with open(version_file, encoding="utf-8") as f:
return f.read().split('"')[1]
configuration = {}
configuration: dict[str, list[str]] = {}
PILLOW_VERSION = get_version()
@ -143,7 +145,7 @@ class RequiredDependencyException(Exception):
PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version
def _dbg(s, tp=None):
def _dbg(s: str, tp: Any = None) -> None:
if DEBUG:
if tp:
print(s % tp)
@ -151,10 +153,13 @@ def _dbg(s, tp=None):
print(s)
def _find_library_dirs_ldconfig():
def _find_library_dirs_ldconfig() -> list[str]:
# Based on ctypes.util from Python 2
ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig"
args: list[str]
env: dict[str, str]
expr: str
if sys.platform.startswith("linux") or sys.platform.startswith("gnu"):
if struct.calcsize("l") == 4:
machine = os.uname()[4] + "-32"
@ -184,13 +189,11 @@ def _find_library_dirs_ldconfig():
try:
p = subprocess.Popen(
args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env
args, stderr=subprocess.DEVNULL, stdout=subprocess.PIPE, env=env, text=True
)
except OSError: # E.g. command not found
return []
[data, _] = p.communicate()
if isinstance(data, bytes):
data = data.decode("latin1")
data = p.communicate()[0]
dirs = []
for dll in re.findall(expr, data):
@ -200,7 +203,9 @@ def _find_library_dirs_ldconfig():
return dirs
def _add_directory(path, subdir, where=None):
def _add_directory(
path: list[str], subdir: str | None, where: int | None = None
) -> None:
if subdir is None:
return
subdir = os.path.realpath(subdir)
@ -216,7 +221,7 @@ def _add_directory(path, subdir, where=None):
path.insert(where, subdir)
def _find_include_file(self, include):
def _find_include_file(self: pil_build_ext, include: str) -> int:
for directory in self.compiler.include_dirs:
_dbg("Checking for include file %s in %s", (include, directory))
if os.path.isfile(os.path.join(directory, include)):
@ -225,7 +230,7 @@ def _find_include_file(self, include):
return 0
def _find_library_file(self, library):
def _find_library_file(self: pil_build_ext, library: str) -> str | None:
ret = self.compiler.find_library_file(self.compiler.library_dirs, library)
if ret:
_dbg("Found library %s at %s", (library, ret))
@ -234,7 +239,7 @@ def _find_library_file(self, library):
return ret
def _find_include_dir(self, dirname, include):
def _find_include_dir(self: pil_build_ext, dirname: str, include: str) -> bool | str:
for directory in self.compiler.include_dirs:
_dbg("Checking for include file %s in %s", (include, directory))
if os.path.isfile(os.path.join(directory, include)):
@ -245,6 +250,7 @@ def _find_include_dir(self, dirname, include):
if os.path.isfile(os.path.join(subdir, include)):
_dbg("Found %s in %s", (include, subdir))
return subdir
return False
def _cmd_exists(cmd: str) -> bool:
@ -256,7 +262,7 @@ def _cmd_exists(cmd: str) -> bool:
)
def _pkg_config(name):
def _pkg_config(name: str) -> tuple[list[str], list[str]] | None:
command = os.environ.get("PKG_CONFIG", "pkg-config")
for keep_system in (True, False):
try:
@ -283,10 +289,11 @@ def _pkg_config(name):
return libs, cflags
except Exception:
pass
return None
class pil_build_ext(build_ext):
class feature:
class ext_feature:
features = [
"zlib",
"jpeg",
@ -301,25 +308,32 @@ class pil_build_ext(build_ext):
]
required = {"jpeg", "zlib"}
vendor = set()
vendor: set[str] = set()
def __init__(self):
def __init__(self) -> None:
self._settings: dict[str, str | bool | None] = {}
for f in self.features:
setattr(self, f, None)
self.set(f, None)
def require(self, feat):
def require(self, feat: str) -> bool:
return feat in self.required
def want(self, feat):
return getattr(self, feat) is None
def get(self, feat: str) -> str | bool | None:
return self._settings[feat]
def want_vendor(self, feat):
def set(self, feat: str, value: str | bool | None) -> None:
self._settings[feat] = value
def want(self, feat: str) -> bool:
return self._settings[feat] is None
def want_vendor(self, feat: str) -> bool:
return feat in self.vendor
def __iter__(self):
def __iter__(self) -> Iterator[str]:
yield from self.features
feature = feature()
feature = ext_feature()
user_options = (
build_ext.user_options
@ -337,10 +351,10 @@ class pil_build_ext(build_ext):
)
@staticmethod
def check_configuration(option, value):
def check_configuration(option: str, value: str) -> bool | None:
return True if value in configuration.get(option, []) else None
def initialize_options(self):
def initialize_options(self) -> None:
self.disable_platform_guessing = self.check_configuration(
"platform-guessing", "disable"
)
@ -355,7 +369,7 @@ class pil_build_ext(build_ext):
self.debug = True
self.parallel = configuration.get("parallel", [None])[-1]
def finalize_options(self):
def finalize_options(self) -> None:
build_ext.finalize_options(self)
if self.debug:
global DEBUG
@ -363,12 +377,16 @@ class pil_build_ext(build_ext):
if not self.parallel:
# If --parallel (or -j) wasn't specified, we want to reproduce the same
# behavior as before, that is, auto-detect the number of jobs.
self.parallel = None
cpu_count = os.cpu_count()
if cpu_count is not None:
try:
self.parallel = int(
os.environ.get("MAX_CONCURRENCY", min(4, os.cpu_count()))
os.environ.get("MAX_CONCURRENCY", min(4, cpu_count))
)
except TypeError:
self.parallel = None
pass
for x in self.feature:
if getattr(self, f"disable_{x}"):
setattr(self.feature, x, False)
@ -402,7 +420,13 @@ class pil_build_ext(build_ext):
_dbg("Using vendored version of %s", x)
self.feature.vendor.add(x)
def _update_extension(self, name, libraries, define_macros=None, sources=None):
def _update_extension(
self,
name: str,
libraries: list[str] | list[str | bool | None],
define_macros: list[tuple[str, str | None]] | None = None,
sources: list[str] | None = None,
) -> None:
for extension in self.extensions:
if extension.name == name:
extension.libraries += libraries
@ -415,13 +439,13 @@ class pil_build_ext(build_ext):
extension.extra_link_args = ["--stdlib=libc++"]
break
def _remove_extension(self, name):
def _remove_extension(self, name: str) -> None:
for extension in self.extensions:
if extension.name == name:
self.extensions.remove(extension)
break
def get_macos_sdk_path(self):
def get_macos_sdk_path(self) -> str | None:
try:
sdk_path = (
subprocess.check_output(["xcrun", "--show-sdk-path"])
@ -442,9 +466,9 @@ class pil_build_ext(build_ext):
sdk_path = commandlinetools_sdk_path
return sdk_path
def build_extensions(self):
library_dirs = []
include_dirs = []
def build_extensions(self) -> None:
library_dirs: list[str] = []
include_dirs: list[str] = []
pkg_config = None
if _cmd_exists(os.environ.get("PKG_CONFIG", "pkg-config")):
@ -468,19 +492,22 @@ class pil_build_ext(build_ext):
root = globals()[root_name]
if root is None and root_name in os.environ:
prefix = os.environ[root_name]
root = (os.path.join(prefix, "lib"), os.path.join(prefix, "include"))
root_prefix = os.environ[root_name]
root = (
os.path.join(root_prefix, "lib"),
os.path.join(root_prefix, "include"),
)
if root is None and pkg_config:
if isinstance(lib_name, tuple):
if isinstance(lib_name, str):
_dbg(f"Looking for `{lib_name}` using pkg-config.")
root = pkg_config(lib_name)
else:
for lib_name2 in lib_name:
_dbg(f"Looking for `{lib_name2}` using pkg-config.")
root = pkg_config(lib_name2)
if root:
break
else:
_dbg(f"Looking for `{lib_name}` using pkg-config.")
root = pkg_config(lib_name)
if isinstance(root, tuple):
lib_root, include_root = root
@ -660,22 +687,22 @@ class pil_build_ext(build_ext):
_dbg("Looking for zlib")
if _find_include_file(self, "zlib.h"):
if _find_library_file(self, "z"):
feature.zlib = "z"
feature.set("zlib", "z")
elif sys.platform == "win32" and _find_library_file(self, "zlib"):
feature.zlib = "zlib" # alternative name
feature.set("zlib", "zlib") # alternative name
if feature.want("jpeg"):
_dbg("Looking for jpeg")
if _find_include_file(self, "jpeglib.h"):
if _find_library_file(self, "jpeg"):
feature.jpeg = "jpeg"
feature.set("jpeg", "jpeg")
elif sys.platform == "win32" and _find_library_file(self, "libjpeg"):
feature.jpeg = "libjpeg" # alternative name
feature.set("jpeg", "libjpeg") # alternative name
feature.openjpeg_version = None
feature.set("openjpeg_version", None)
if feature.want("jpeg2000"):
_dbg("Looking for jpeg2000")
best_version = None
best_version: tuple[int, ...] | None = None
best_path = None
# Find the best version
@ -705,26 +732,26 @@ class pil_build_ext(build_ext):
# <openjpeg.h> rather than having to cope with the versioned
# include path
_add_directory(self.compiler.include_dirs, best_path, 0)
feature.jpeg2000 = "openjp2"
feature.openjpeg_version = ".".join(str(x) for x in best_version)
feature.set("jpeg2000", "openjp2")
feature.set("openjpeg_version", ".".join(str(x) for x in best_version))
if feature.want("imagequant"):
_dbg("Looking for imagequant")
if _find_include_file(self, "libimagequant.h"):
if _find_library_file(self, "imagequant"):
feature.imagequant = "imagequant"
feature.set("imagequant", "imagequant")
elif _find_library_file(self, "libimagequant"):
feature.imagequant = "libimagequant"
feature.set("imagequant", "libimagequant")
if feature.want("tiff"):
_dbg("Looking for tiff")
if _find_include_file(self, "tiff.h"):
if _find_library_file(self, "tiff"):
feature.tiff = "tiff"
feature.set("tiff", "tiff")
if sys.platform in ["win32", "darwin"] and _find_library_file(
self, "libtiff"
):
feature.tiff = "libtiff"
feature.set("tiff", "libtiff")
if feature.want("freetype"):
_dbg("Looking for freetype")
@ -745,31 +772,31 @@ class pil_build_ext(build_ext):
freetype_version = 21
break
if freetype_version:
feature.freetype = "freetype"
feature.set("freetype", "freetype")
if subdir:
_add_directory(self.compiler.include_dirs, subdir, 0)
if feature.freetype and feature.want("raqm"):
if feature.get("freetype") and feature.want("raqm"):
if not feature.want_vendor("raqm"): # want system Raqm
_dbg("Looking for Raqm")
if _find_include_file(self, "raqm.h"):
if _find_library_file(self, "raqm"):
feature.raqm = "raqm"
feature.set("raqm", "raqm")
elif _find_library_file(self, "libraqm"):
feature.raqm = "libraqm"
feature.set("raqm", "libraqm")
else: # want to build Raqm from src/thirdparty
_dbg("Looking for HarfBuzz")
feature.harfbuzz = None
feature.set("harfbuzz", None)
hb_dir = _find_include_dir(self, "harfbuzz", "hb.h")
if hb_dir:
if isinstance(hb_dir, str):
_add_directory(self.compiler.include_dirs, hb_dir, 0)
if _find_library_file(self, "harfbuzz"):
feature.harfbuzz = "harfbuzz"
if feature.harfbuzz:
feature.set("harfbuzz", "harfbuzz")
if feature.get("harfbuzz"):
if not feature.want_vendor("fribidi"): # want system FriBiDi
_dbg("Looking for FriBiDi")
feature.fribidi = None
feature.set("fribidi", None)
fribidi_dir = _find_include_dir(self, "fribidi", "fribidi.h")
if fribidi_dir:
if isinstance(fribidi_dir, str):
@ -777,19 +804,19 @@ class pil_build_ext(build_ext):
self.compiler.include_dirs, fribidi_dir, 0
)
if _find_library_file(self, "fribidi"):
feature.fribidi = "fribidi"
feature.raqm = True
feature.set("fribidi", "fribidi")
feature.set("raqm", True)
else: # want to build FriBiDi shim from src/thirdparty
feature.raqm = True
feature.set("raqm", True)
if feature.want("lcms"):
_dbg("Looking for lcms")
if _find_include_file(self, "lcms2.h"):
if _find_library_file(self, "lcms2"):
feature.lcms = "lcms2"
feature.set("lcms", "lcms2")
elif _find_library_file(self, "lcms2_static"):
# alternate Windows name.
feature.lcms = "lcms2_static"
feature.set("lcms", "lcms2_static")
if feature.want("webp"):
_dbg("Looking for webp")
@ -803,17 +830,17 @@ class pil_build_ext(build_ext):
_find_library_file(self, prefix + library)
for library in ("webp", "webpmux", "webpdemux")
):
feature.webp = prefix + "webp"
feature.set("webp", prefix + "webp")
break
if feature.want("xcb"):
_dbg("Looking for xcb")
if _find_include_file(self, "xcb/xcb.h"):
if _find_library_file(self, "xcb"):
feature.xcb = "xcb"
feature.set("xcb", "xcb")
for f in feature:
if not getattr(feature, f) and feature.require(f):
if not feature.get(f) and feature.require(f):
if f in ("jpeg", "zlib"):
raise RequiredDependencyException(f)
raise DependencyException(f)
@ -821,10 +848,11 @@ class pil_build_ext(build_ext):
#
# core library
libs = self.add_imaging_libs.split()
defs = []
if feature.tiff:
libs.append(feature.tiff)
libs: list[str | bool | None] = []
libs.extend(self.add_imaging_libs.split())
defs: list[tuple[str, str | None]] = []
if feature.get("tiff"):
libs.append(feature.get("tiff"))
defs.append(("HAVE_LIBTIFF", None))
if sys.platform == "win32":
# This define needs to be defined if-and-only-if it was defined
@ -832,22 +860,22 @@ class pil_build_ext(build_ext):
# so we have to guess; by default it is defined in all Windows builds.
# See #4237, #5243, #5359 for more information.
defs.append(("USE_WIN32_FILEIO", None))
if feature.jpeg:
libs.append(feature.jpeg)
if feature.get("jpeg"):
libs.append(feature.get("jpeg"))
defs.append(("HAVE_LIBJPEG", None))
if feature.jpeg2000:
libs.append(feature.jpeg2000)
if feature.get("jpeg2000"):
libs.append(feature.get("jpeg2000"))
defs.append(("HAVE_OPENJPEG", None))
if sys.platform == "win32" and not PLATFORM_MINGW:
defs.append(("OPJ_STATIC", None))
if feature.zlib:
libs.append(feature.zlib)
if feature.get("zlib"):
libs.append(feature.get("zlib"))
defs.append(("HAVE_LIBZ", None))
if feature.imagequant:
libs.append(feature.imagequant)
if feature.get("imagequant"):
libs.append(feature.get("imagequant"))
defs.append(("HAVE_LIBIMAGEQUANT", None))
if feature.xcb:
libs.append(feature.xcb)
if feature.get("xcb"):
libs.append(feature.get("xcb"))
defs.append(("HAVE_XCB", None))
if sys.platform == "win32":
libs.extend(["kernel32", "user32", "gdi32"])
@ -861,22 +889,22 @@ class pil_build_ext(build_ext):
#
# additional libraries
if feature.freetype:
if feature.get("freetype"):
srcs = []
libs = ["freetype"]
defs = []
if feature.raqm:
if feature.get("raqm"):
if not feature.want_vendor("raqm"): # using system Raqm
defs.append(("HAVE_RAQM", None))
defs.append(("HAVE_RAQM_SYSTEM", None))
libs.append(feature.raqm)
libs.append(feature.get("raqm"))
else: # building Raqm from src/thirdparty
defs.append(("HAVE_RAQM", None))
srcs.append("src/thirdparty/raqm/raqm.c")
libs.append(feature.harfbuzz)
libs.append(feature.get("harfbuzz"))
if not feature.want_vendor("fribidi"): # using system FriBiDi
defs.append(("HAVE_FRIBIDI_SYSTEM", None))
libs.append(feature.fribidi)
libs.append(feature.get("fribidi"))
else: # building FriBiDi shim from src/thirdparty
srcs.append("src/thirdparty/fribidi-shim/fribidi.c")
self._update_extension("PIL._imagingft", libs, defs, srcs)
@ -884,16 +912,17 @@ class pil_build_ext(build_ext):
else:
self._remove_extension("PIL._imagingft")
if feature.lcms:
extra = []
if feature.get("lcms"):
libs = [feature.get("lcms")]
if sys.platform == "win32":
extra.extend(["user32", "gdi32"])
self._update_extension("PIL._imagingcms", [feature.lcms] + extra)
libs.extend(["user32", "gdi32"])
self._update_extension("PIL._imagingcms", libs)
else:
self._remove_extension("PIL._imagingcms")
if feature.webp:
libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"]
webp = feature.get("webp")
if isinstance(webp, str):
libs = [webp, webp + "mux", webp + "demux"]
self._update_extension("PIL._webp", libs)
else:
self._remove_extension("PIL._webp")
@ -908,14 +937,14 @@ class pil_build_ext(build_ext):
self.summary_report(feature)
def summary_report(self, feature):
def summary_report(self, feature: ext_feature) -> None:
print("-" * 68)
print("PIL SETUP SUMMARY")
print("-" * 68)
print(f"version Pillow {PILLOW_VERSION}")
v = sys.version.split("[")
print(f"platform {sys.platform} {v[0].strip()}")
for v in v[1:]:
version = sys.version.split("[")
print(f"platform {sys.platform} {version[0].strip()}")
for v in version[1:]:
print(f" [{v.strip()}")
print("-" * 68)
@ -926,16 +955,20 @@ class pil_build_ext(build_ext):
raqm_extra_info += ", FriBiDi shim"
options = [
(feature.jpeg, "JPEG"),
(feature.jpeg2000, "OPENJPEG (JPEG2000)", feature.openjpeg_version),
(feature.zlib, "ZLIB (PNG/ZIP)"),
(feature.imagequant, "LIBIMAGEQUANT"),
(feature.tiff, "LIBTIFF"),
(feature.freetype, "FREETYPE2"),
(feature.raqm, "RAQM (Text shaping)", raqm_extra_info),
(feature.lcms, "LITTLECMS2"),
(feature.webp, "WEBP"),
(feature.xcb, "XCB (X protocol)"),
(feature.get("jpeg"), "JPEG"),
(
feature.get("jpeg2000"),
"OPENJPEG (JPEG2000)",
feature.get("openjpeg_version"),
),
(feature.get("zlib"), "ZLIB (PNG/ZIP)"),
(feature.get("imagequant"), "LIBIMAGEQUANT"),
(feature.get("tiff"), "LIBTIFF"),
(feature.get("freetype"), "FREETYPE2"),
(feature.get("raqm"), "RAQM (Text shaping)", raqm_extra_info),
(feature.get("lcms"), "LITTLECMS2"),
(feature.get("webp"), "WEBP"),
(feature.get("xcb"), "XCB (X protocol)"),
]
all = 1
@ -964,7 +997,7 @@ class pil_build_ext(build_ext):
print("")
def debug_build():
def debug_build() -> bool:
return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD

View File

@ -273,13 +273,13 @@ class BlpImageFile(ImageFile.ImageFile):
raise BLPFormatError(msg)
self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try:
self._read_blp_header()
self._load()
@ -372,7 +372,10 @@ class BLP1Decoder(_BLPBaseDecoder):
Image._decompression_bomb_check(image.size)
if image.mode == "CMYK":
decoder_name, extents, offset, args = image.tile[0]
image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
assert isinstance(args, tuple)
image.tile = [
ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK"))
]
r, g, b = image.convert("RGB").split()
reversed_image = Image.merge("RGB", (b, g, r))
self.set_as_raw(reversed_image.tobytes())
@ -467,6 +470,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
assert im.palette is not None
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))

View File

@ -170,6 +170,8 @@ class BmpImageFile(ImageFile.ImageFile):
# ------------------ Special case : header is reported 40, which
# ---------------------- is shorter than real size for bpp >= 16
assert isinstance(file_info["width"], int)
assert isinstance(file_info["height"], int)
self._size = file_info["width"], file_info["height"]
# ------- If color count was not found in the header, compute from bits
@ -294,7 +296,7 @@ class BmpImageFile(ImageFile.ImageFile):
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"])
self.tile = [
(
ImageFile._Tile(
decoder_name,
(0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(),
@ -319,7 +321,7 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
rle4 = self.args[1]
data = bytearray()
@ -385,7 +387,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR)
rawmode = "L" if self.mode == "L" else "P"
self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1]))
self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
return -1, 0

View File

@ -17,7 +17,7 @@
#
from __future__ import annotations
from . import BmpImagePlugin, Image
from . import BmpImagePlugin, Image, ImageFile
from ._binary import i16le as i16
from ._binary import i32le as i32
@ -64,7 +64,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
# patch up the bitmap height
self._size = self.size[0], self.size[1] // 2
d, e, o, a = self.tile[0]
self.tile[0] = d, (0, 0) + self.size, o, a
self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a)
#

View File

@ -367,7 +367,7 @@ class DdsImageFile(ImageFile.ImageFile):
mask_count = 3
masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))]
return
elif pfflags & DDPF.LUMINANCE:
if bitcount == 8:
@ -481,7 +481,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
bitcount, masks = self.args

View File

@ -71,7 +71,7 @@ def Ghostscript(
fp: IO[bytes],
scale: int = 1,
transparency: bool = False,
) -> Image.Image:
) -> Image.core.ImagingCore:
"""Render an image using Ghostscript"""
global gs_binary
if not has_ghostscript():
@ -190,7 +190,6 @@ class EpsImageFile(ImageFile.ImageFile):
self.fp.seek(offset)
self._mode = "RGB"
self._size = None
byte_arr = bytearray(255)
bytes_mv = memoryview(byte_arr)
@ -228,7 +227,7 @@ class EpsImageFile(ImageFile.ImageFile):
if k == "BoundingBox":
if v == "(atend)":
reading_trailer_comments = True
elif not self._size or (trailer_reached and reading_trailer_comments):
elif not self.tile or (trailer_reached and reading_trailer_comments):
try:
# Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers
@ -346,7 +345,7 @@ class EpsImageFile(ImageFile.ImageFile):
trailer_reached = True
bytes_read = 0
if not self._size:
if not self.tile:
msg = "cannot determine EPS bounding box"
raise OSError(msg)

View File

@ -67,7 +67,7 @@ class FitsImageFile(ImageFile.ImageFile):
raise ValueError(msg)
offset += self.fp.tell() - 80
self.tile = [(decoder_name, (0, 0) + self.size, offset, args)]
self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)]
def _get_size(
self, headers: dict[bytes, bytes], prefix: bytes
@ -126,7 +126,7 @@ class FitsImageFile(ImageFile.ImageFile):
class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
value = gzip.decompress(self.fd.read())

View File

@ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile):
framesize = i32(s)
self.decodermaxblock = framesize
self.tile = [("fli", (0, 0) + self.size, self.__offset, None)]
self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)]
self.__offset += framesize

View File

@ -81,6 +81,8 @@ class FpxImageFile(ImageFile.ImageFile):
# size (highest resolution)
assert isinstance(prop[0x1000002], int)
assert isinstance(prop[0x1000003], int)
self._size = prop[0x1000002], prop[0x1000003]
size = max(self.size)
@ -164,7 +166,7 @@ class FpxImageFile(ImageFile.ImageFile):
if compression == 0:
self.tile.append(
(
ImageFile._Tile(
"raw",
(x, y, x1, y1),
i32(s, i) + 28,
@ -175,7 +177,7 @@ class FpxImageFile(ImageFile.ImageFile):
elif compression == 1:
# FIXME: the fill decoder is not implemented
self.tile.append(
(
ImageFile._Tile(
"fill",
(x, y, x1, y1),
i32(s, i) + 28,
@ -203,7 +205,7 @@ class FpxImageFile(ImageFile.ImageFile):
jpegmode = rawmode
self.tile.append(
(
ImageFile._Tile(
"jpeg",
(x, y, x1, y1),
i32(s, i) + 28,

View File

@ -93,9 +93,9 @@ class FtexImageFile(ImageFile.ImageFile):
if format == Format.DXT1:
self._mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, 1)]
self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
else:
msg = f"Invalid texture compression format: {repr(format)}"
raise ValueError(msg)

View File

@ -89,7 +89,7 @@ class GbrImageFile(ImageFile.ImageFile):
self._data_size = width * height * color_depth
def load(self) -> Image.core.PixelAccess | None:
if not self.im:
if self._im is None:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size))
return Image.Image.load(self)

View File

@ -72,7 +72,7 @@ class GdImageFile(ImageFile.ImageFile):
)
self.tile = [
(
ImageFile._Tile(
"raw",
(0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4,

View File

@ -29,7 +29,6 @@ import itertools
import math
import os
import subprocess
import sys
from enum import IntEnum
from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
@ -49,6 +48,7 @@ from ._binary import o16le as o16
if TYPE_CHECKING:
from . import _imaging
from ._typing import Buffer
class LoadingStrategy(IntEnum):
@ -155,7 +155,7 @@ class GifImageFile(ImageFile.ImageFile):
if not self._seek_check(frame):
return
if frame < self.__frame:
self.im = None
self._im = None
self._seek(0)
last_frame = self.__frame
@ -320,11 +320,14 @@ class GifImageFile(ImageFile.ImageFile):
else:
self._mode = "L"
if not palette and self.global_palette:
if palette:
self.palette = palette
elif self.global_palette:
from copy import copy
palette = copy(self.global_palette)
self.palette = palette
self.palette = copy(self.global_palette)
else:
self.palette = None
else:
if self.mode == "P":
if (
@ -376,7 +379,7 @@ class GifImageFile(ImageFile.ImageFile):
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
else:
# replace with previous contents
if self.im is not None:
if self._im is not None:
# only dispose the extent in this frame
self.dispose = self._crop(self.im, self.dispose_extent)
elif frame_transparency is not None:
@ -404,7 +407,7 @@ class GifImageFile(ImageFile.ImageFile):
elif self.mode not in ("RGB", "RGBA"):
transparency = frame_transparency
self.tile = [
(
ImageFile._Tile(
"gif",
(x0, y0, x1, y1),
self.__offset,
@ -434,7 +437,14 @@ class GifImageFile(ImageFile.ImageFile):
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
self.im.putpalette("RGB", *self._frame_palette.getdata())
else:
self.im = None
self._im = None
if not self._prev_im and self._im is not None and self.size != self.im.size:
expanded_im = Image.core.fill(self.im.mode, self.size)
if self._frame_palette:
expanded_im.putpalette("RGB", *self._frame_palette.getdata())
expanded_im.paste(self.im, (0, 0) + self.im.size)
self.im = expanded_im
self._mode = temp_mode
self._frame_palette = None
@ -452,6 +462,17 @@ class GifImageFile(ImageFile.ImageFile):
return
if not self._prev_im:
return
if self.size != self._prev_im.size:
if self._frame_transparency is not None:
expanded_im = Image.core.fill("RGBA", self.size)
else:
expanded_im = Image.core.fill("P", self.size)
expanded_im.putpalette("RGB", "RGB", self.im.getpalette())
expanded_im = expanded_im.convert("RGB")
expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)
self._prev_im = expanded_im
assert self._prev_im is not None
if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0)
frame_im = self.im.convert("RGBA")
@ -495,6 +516,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
return im
if Image.getmodebase(im.mode) == "RGB":
im = im.convert("P", palette=Image.Palette.ADAPTIVE)
assert im.palette is not None
if im.palette.mode == "RGBA":
for rgba in im.palette.colors:
if rgba[3] == 0:
@ -531,16 +553,18 @@ def _normalize_palette(
if im.mode == "P":
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
if not source_palette:
source_palette = bytearray(i // 3 for i in range(768))
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
used_palette_colors: list[int] | None
if palette:
used_palette_colors = []
assert source_palette is not None
if palette:
used_palette_colors: list[int | None] = []
assert im.palette is not None
for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3])
index = im.palette.colors.get(source_color)
@ -553,20 +577,25 @@ def _normalize_palette(
if j not in used_palette_colors:
used_palette_colors[i] = j
break
im = im.remap_palette(used_palette_colors)
dest_map: list[int] = []
for index in used_palette_colors:
assert index is not None
dest_map.append(index)
im = im.remap_palette(dest_map)
else:
used_palette_colors = _get_optimize(im, info)
if used_palette_colors is not None:
im = im.remap_palette(used_palette_colors, source_palette)
optimized_palette_colors = _get_optimize(im, info)
if optimized_palette_colors is not None:
im = im.remap_palette(optimized_palette_colors, source_palette)
if "transparency" in info:
try:
info["transparency"] = used_palette_colors.index(
info["transparency"] = optimized_palette_colors.index(
info["transparency"]
)
except ValueError:
del info["transparency"]
return im
assert im.palette is not None
im.palette.palette = source_palette
return im
@ -578,6 +607,7 @@ def _write_single_frame(
) -> None:
im_out = _normalize_mode(im)
for k, v in im_out.info.items():
if isinstance(k, str):
im.encoderinfo.setdefault(k, v)
im_out = _normalize_palette(im_out, palette, im.encoderinfo)
@ -601,7 +631,10 @@ def _write_single_frame(
def _getbbox(
base_im: Image.Image, im_frame: Image.Image
) -> 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")
base_im = base_im.convert("RGBA")
delta = ImageChops.subtract_modulo(im_frame, base_im)
@ -632,6 +665,7 @@ def _write_multiple_frames(
for k, v in im_frame.info.items():
if k == "transparency":
continue
if isinstance(k, str):
im.encoderinfo.setdefault(k, v)
encoderinfo = im.encoderinfo.copy()
@ -662,10 +696,12 @@ def _write_multiple_frames(
)
background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background)
assert im_frames[0].im.palette is not None
background_im.putpalette(im_frames[0].im.palette)
bbox = _getbbox(background_im, im_frame)[1]
elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo:
assert im_frame.palette is not None
try:
encoderinfo["transparency"] = (
im_frame.palette._new_color_index(im_frame)
@ -903,6 +939,7 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
if optimise or max(used_palette_colors) >= len(used_palette_colors):
return used_palette_colors
assert im.palette is not None
num_palette_colors = len(im.palette.palette) // Image.getmodebands(
im.palette.mode
)
@ -952,7 +989,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
:param im: Image object
:returns: Bytes, len<=768 suitable for inclusion in gif header
"""
return 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(
@ -965,6 +1008,7 @@ def _get_background(
# WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's
# info["background"] - a global color table index
assert im.palette is not None
try:
background = im.palette.getcolor(info_background, im)
except ValueError as e:
@ -1124,19 +1168,10 @@ def getdata(
class Collector(BytesIO):
data = []
if sys.version_info >= (3, 12):
from collections.abc import Buffer
def write(self, data: Buffer) -> int:
self.data.append(data)
return len(data)
else:
def write(self, data: Any) -> int:
self.data.append(data)
return len(data)
im.load() # make sure raster data is available
fp = Collector()

View File

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

View File

@ -228,7 +228,7 @@ class IcoFile:
# change tile dimension to only encompass XOR image
im._size = (im.size[0], int(im.size[1] / 2))
d, e, o, a = im.tile[0]
im.tile[0] = d, (0, 0) + im.size, o, a
im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a)
# figure out where AND mask image starts
if header.bpp == 32:
@ -243,6 +243,7 @@ class IcoFile:
alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
# convert to an 8bpp grayscale image
try:
mask = Image.frombuffer(
"L", # 8bpp
im.size, # (w, h)
@ -250,6 +251,11 @@ class IcoFile:
"raw", # raw decoder
("L", 0, -1), # 8bpp inverted, unpadded, reversed
)
except ValueError:
if ImageFile.LOAD_TRUNCATED_IMAGES:
mask = None
else:
raise
else:
# get AND image from end of bitmap
w = im.size[0]
@ -267,6 +273,7 @@ class IcoFile:
mask_data = self.buf.read(total_bytes)
# convert raw data to image
try:
mask = Image.frombuffer(
"1", # 1 bpp
im.size, # (w, h)
@ -274,10 +281,16 @@ class IcoFile:
"raw", # raw decoder
("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
)
except ValueError:
if ImageFile.LOAD_TRUNCATED_IMAGES:
mask = None
else:
raise
# now we have two images, im is XOR image and mask is AND image
# apply mask image as alpha channel
if mask:
im = im.convert("RGBA")
im.putalpha(mask)
@ -319,18 +332,18 @@ class IcoImageFile(ImageFile.ImageFile):
self.load()
@property
def size(self):
def size(self) -> tuple[int, int]:
return self._size
@size.setter
def size(self, value):
def size(self, value: tuple[int, int]) -> None:
if value not in self.info["sizes"]:
msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg)
self._size = value
def load(self) -> Image.core.PixelAccess | None:
if self.im is not None and self.im.size == self.size:
if self._im is not None and self.im.size == self.size:
# Already loaded
return Image.Image.load(self)
im = self.ico.getimage(self.size)

View File

@ -253,7 +253,11 @@ class ImImageFile(ImageFile.ImageFile):
# use bit decoder (if necessary)
bits = int(self.rawmode[2:])
if bits not in [8, 16, 32]:
self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))]
self.tile = [
ImageFile._Tile(
"bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
)
]
return
except ValueError:
pass
@ -263,13 +267,17 @@ class ImImageFile(ImageFile.ImageFile):
# ever stumbled upon such a file ;-)
size = self.size[0] * self.size[1]
self.tile = [
("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)),
ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
ImageFile._Tile(
"raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
),
]
else:
# LabEye/IFUNC files
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
]
@property
def n_frames(self) -> int:
@ -295,7 +303,9 @@ class ImImageFile(ImageFile.ImageFile):
self.fp = self._fp
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
]
def tell(self) -> int:
return self.frame

View File

@ -133,6 +133,7 @@ def isImageType(t: Any) -> TypeGuard[Image]:
:param t: object to check if it's an image
:returns: True if the object is an image
"""
deprecate("Image.isImageType(im)", 12, "isinstance(im, Image.Image)")
return hasattr(t, "im")
@ -221,8 +222,15 @@ if TYPE_CHECKING:
import mmap
from xml.etree.ElementTree import Element
from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
if sys.version_info >= (3, 13):
from types import CapsuleType
else:
CapsuleType = object
ID: list[str] = []
OPEN: dict[
str,
@ -468,43 +476,53 @@ def _getencoder(
# Simple expression analyzer
class _E:
class ImagePointTransform:
"""
Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than
8 bits, this represents an affine transformation, where the value is multiplied by
``scale`` and ``offset`` is added.
"""
def __init__(self, scale: float, offset: float) -> None:
self.scale = scale
self.offset = offset
def __neg__(self) -> _E:
return _E(-self.scale, -self.offset)
def __neg__(self) -> ImagePointTransform:
return ImagePointTransform(-self.scale, -self.offset)
def __add__(self, other: _E | float) -> _E:
if isinstance(other, _E):
return _E(self.scale + other.scale, self.offset + other.offset)
return _E(self.scale, self.offset + other)
def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return ImagePointTransform(
self.scale + other.scale, self.offset + other.offset
)
return ImagePointTransform(self.scale, self.offset + other)
__radd__ = __add__
def __sub__(self, other: _E | float) -> _E:
def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return self + -other
def __rsub__(self, other: _E | float) -> _E:
def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return other + -self
def __mul__(self, other: _E | float) -> _E:
if isinstance(other, _E):
def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return NotImplemented
return _E(self.scale * other, self.offset * other)
return ImagePointTransform(self.scale * other, self.offset * other)
__rmul__ = __mul__
def __truediv__(self, other: _E | float) -> _E:
if isinstance(other, _E):
def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return NotImplemented
return _E(self.scale / other, self.offset / other)
return ImagePointTransform(self.scale / other, self.offset / other)
def _getscaleoffset(expr) -> tuple[float, float]:
a = expr(_E(1, 0))
return (a.scale, a.offset) if isinstance(a, _E) else (0, a)
def _getscaleoffset(
expr: Callable[[ImagePointTransform], ImagePointTransform | float]
) -> tuple[float, float]:
a = expr(ImagePointTransform(1, 0))
return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)
# --------------------------------------------------------------------
@ -533,16 +551,27 @@ class Image:
format_description: str | None = None
_close_exclusive_fp_after_loading = True
def __init__(self):
def __init__(self) -> None:
# FIXME: take "new" parameters / other image?
# FIXME: turn mode and size into delegating properties?
self.im = None
self._im: core.ImagingCore | DeferredError | None = None
self._mode = ""
self._size = (0, 0)
self.palette = None
self.info = {}
self.palette: ImagePalette.ImagePalette | None = None
self.info: dict[str | tuple[int, int], Any] = {}
self.readonly = 0
self._exif = None
self._exif: Exif | None = None
@property
def im(self) -> core.ImagingCore:
if isinstance(self._im, DeferredError):
raise self._im.ex
assert self._im is not None
return self._im
@im.setter
def im(self, im: core.ImagingCore) -> None:
self._im = im
@property
def width(self) -> int:
@ -618,7 +647,7 @@ class Image:
# Instead of simply setting to None, we're setting up a
# deferred error that will better explain that the core image
# object is gone.
self.im = DeferredError(ValueError("Operation on closed image"))
self._im = DeferredError(ValueError("Operation on closed image"))
def _copy(self) -> None:
self.load()
@ -677,7 +706,7 @@ class Image:
id(self),
)
def _repr_pretty_(self, p, cycle: bool) -> None:
def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
"""IPython plain text display support"""
# Same as __repr__ but without unpredictable id(self),
@ -724,24 +753,12 @@ class Image:
def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]:
# numpy array interface support
new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3}
try:
if self.mode == "1":
# Binary images need to be extended from bits to bytes
# See: https://github.com/python-pillow/Pillow/issues/350
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
except Exception as e:
if not isinstance(e, (MemoryError, RecursionError)):
try:
import numpy
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if parse_version(numpy.__version__) < parse_version("1.23"):
warnings.warn(str(e))
raise
new["shape"], new["typestr"] = _conv_type_shape(self)
return new
@ -840,7 +857,10 @@ class Image:
)
def frombytes(
self, data: bytes | bytearray, decoder_name: str = "raw", *args: Any
self,
data: bytes | bytearray | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> None:
"""
Loads this image with pixel data from a bytes object.
@ -888,7 +908,7 @@ class Image:
:returns: An image access object.
:rtype: :py:class:`.PixelAccess`
"""
if self.im is not None and self.palette and self.palette.dirty:
if self._im is not None and self.palette and self.palette.dirty:
# realize palette
mode, arr = self.palette.getdata()
self.im.putpalette(self.palette.mode, mode, arr)
@ -905,7 +925,7 @@ class Image:
self.palette.mode, self.palette.mode
)
if self.im is not None:
if self._im is not None:
return self.im.pixel_access(self.readonly)
return None
@ -1046,9 +1066,11 @@ class Image:
# use existing conversions
trns_im = new(self.mode, (1, 1))
if self.mode == "P":
trns_im.putpalette(self.palette)
assert self.palette is not None
trns_im.putpalette(self.palette, self.palette.mode)
if isinstance(t, tuple):
err = "Couldn't allocate a palette color for transparency"
assert trns_im.palette is not None
try:
t = trns_im.palette.getcolor(t, self)
except ValueError as e:
@ -1109,17 +1131,23 @@ class Image:
return new_im
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"):
from . import ImageCms
srgb = ImageCms.createProfile("sRGB")
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(
profiles[0], profiles[1], self.mode, mode
profiles[0], profiles[1], im.mode, mode
)
return transform.apply(self)
return transform.apply(im)
# colorspace conversion
if dither is None:
@ -1150,7 +1178,9 @@ class Image:
if trns is not None:
if new_im.mode == "P" and new_im.palette:
try:
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
new_im.info["transparency"] = new_im.palette.getcolor(
cast(tuple[int, ...], trns), new_im # trns was converted to RGB
)
except ValueError as e:
del new_im.info["transparency"]
if str(e) != "cannot allocate more than 256 colors":
@ -1228,6 +1258,7 @@ class Image:
raise ValueError(msg)
im = self.im.convert("P", dither, palette.im)
new_im = self._new(im)
assert palette.palette is not None
new_im.palette = palette.palette.copy()
return new_im
@ -1582,7 +1613,7 @@ class Image:
self.fp.seek(offset)
return child_images
def getim(self):
def getim(self) -> CapsuleType:
"""
Returns a capsule that points to the internal image memory.
@ -1626,11 +1657,15 @@ class Image:
:returns: A boolean.
"""
return (
if (
self.mode in ("LA", "La", "PA", "RGBA", "RGBa")
or (self.mode == "P" and self.palette.mode.endswith("A"))
or "transparency" in self.info
)
):
return True
if self.mode == "P":
assert self.palette is not None
return self.palette.mode.endswith("A")
return False
def apply_transparency(self) -> None:
"""
@ -1789,23 +1824,22 @@ class Image:
:param mask: An optional mask image.
"""
if isImageType(box):
if isinstance(box, Image):
if mask is not None:
msg = "If using second argument as mask, third argument must be None"
raise ValueError(msg)
# abbreviated paste(im, mask) syntax
mask = box
box = None
assert not isinstance(box, Image)
if box is None:
box = (0, 0)
if len(box) == 2:
# upper left corner given; get size from image or mask
if isImageType(im):
if isinstance(im, Image):
size = im.size
elif isImageType(mask):
elif isinstance(mask, Image):
size = mask.size
else:
# FIXME: use self.size here?
@ -1813,26 +1847,28 @@ class Image:
raise ValueError(msg)
box += (box[0] + size[0], box[1] + size[1])
source: core.ImagingCore | str | float | tuple[float, ...]
if isinstance(im, str):
from . import ImageColor
im = ImageColor.getcolor(im, self.mode)
elif isImageType(im):
source = ImageColor.getcolor(im, self.mode)
elif isinstance(im, Image):
im.load()
if self.mode != im.mode:
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
# should use an adapter for this!
im = im.convert(self.mode)
im = im.im
source = im.im
else:
source = im
self._ensure_mutable()
if mask:
mask.load()
self.im.paste(im, box, mask.im)
self.im.paste(source, box, mask.im)
else:
self.im.paste(im, box)
self.im.paste(source, box)
def alpha_composite(
self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0)
@ -1892,7 +1928,13 @@ class Image:
def point(
self,
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
lut: (
Sequence[float]
| NumpyArray
| Callable[[int], float]
| Callable[[ImagePointTransform], ImagePointTransform | float]
| ImagePointHandler
),
mode: str | None = None,
) -> Image:
"""
@ -1909,7 +1951,7 @@ class Image:
object::
class Example(Image.ImagePointHandler):
def point(self, data):
def point(self, im: Image) -> Image:
# Return result
:param mode: Output mode (default is same as input). This can only be used if
the source image has mode "L" or "P", and the output has mode "1" or the
@ -1928,10 +1970,10 @@ class Image:
# check if the function can be used with point_transform
# UNDONE wiredfool -- I think this prevents us from ever doing
# a gamma function point transform on > 8bit images.
scale, offset = _getscaleoffset(lut)
scale, offset = _getscaleoffset(lut) # type: ignore[arg-type]
return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table
flatLut = [lut(i) for i in range(256)] * self.im.bands
flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type]
else:
flatLut = lut
@ -1979,7 +2021,7 @@ class Image:
else:
band = 3
if isImageType(alpha):
if isinstance(alpha, Image):
# alpha layer
if alpha.mode not in ("1", "L"):
msg = "illegal image mode"
@ -1989,7 +2031,6 @@ class Image:
alpha = alpha.convert("L")
else:
# constant alpha
alpha = cast(int, alpha) # see python/typing#1013
try:
self.im.fillband(band, alpha)
except (AttributeError, ValueError):
@ -2103,7 +2144,8 @@ class Image:
if self.mode == "PA":
alpha = value[3] if len(value) == 4 else 255
value = value[:3]
palette_index = self.palette.getcolor(value, self)
assert self.palette is not None
palette_index = self.palette.getcolor(tuple(value), self)
value = (palette_index, alpha) if self.mode == "PA" else palette_index
return self.im.putpixel(xy, value)
@ -2137,6 +2179,9 @@ class Image:
source_palette = self.im.getpalette(palette_mode, palette_mode)
else: # L-mode
source_palette = bytearray(i // 3 for i in range(768))
elif len(source_palette) > 768:
bands = 4
palette_mode = "RGBA"
palette_bytes = b""
new_positions = [0] * 256
@ -2287,7 +2332,6 @@ class Image:
msg = "reducing_gap must be 1.0 or greater"
raise ValueError(msg)
self.load()
if box is None:
box = (0, 0) + self.size
@ -2736,27 +2780,18 @@ class Image:
)
return x, y
box = None
final_size: tuple[int, int]
if reducing_gap is not None:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
final_size = preserved_size
box = None
if reducing_gap is not None:
res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
)
if res is not None:
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:
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
@ -2867,11 +2902,11 @@ class Image:
self,
box: tuple[int, int, int, int],
image: Image,
method,
data,
method: Transform,
data: Sequence[float],
resample: int = Resampling.NEAREST,
fill: bool = True,
):
) -> None:
w = box[2] - box[0]
h = box[3] - box[1]
@ -2972,7 +3007,7 @@ class Image:
self.load()
return self._new(self.im.effect_spread(distance))
def toqimage(self):
def toqimage(self) -> ImageQt.ImageQt:
"""Returns a QImage copy of this image"""
from . import ImageQt
@ -2981,7 +3016,7 @@ class Image:
raise ImportError(msg)
return ImageQt.toqimage(self)
def toqpixmap(self):
def toqpixmap(self) -> ImageQt.QPixmap:
"""Returns a QPixmap copy of this image"""
from . import ImageQt
@ -3109,7 +3144,7 @@ def new(
def frombytes(
mode: str,
size: tuple[int, int],
data: bytes | bytearray,
data: bytes | bytearray | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> Image:
@ -3153,7 +3188,11 @@ def frombytes(
def frombuffer(
mode: str, size: tuple[int, int], data, decoder_name: str = "raw", *args: Any
mode: str,
size: tuple[int, int],
data: bytes | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> Image:
"""
Creates an image memory referencing pixel data in a byte buffer.
@ -3308,7 +3347,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
def fromqimage(im) -> ImageFile.ImageFile:
def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image"""
from . import ImageQt
@ -3318,7 +3357,7 @@ def fromqimage(im) -> ImageFile.ImageFile:
return ImageQt.fromqimage(im)
def fromqpixmap(im) -> ImageFile.ImageFile:
def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile:
"""Creates an image instance from a QPixmap image"""
from . import ImageQt
@ -3610,7 +3649,10 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
def register_open(
id: str,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
factory: (
Callable[[IO[bytes], str | bytes], ImageFile.ImageFile]
| type[ImageFile.ImageFile]
),
accept: Callable[[bytes], bool | str] | None = None,
) -> None:
"""
@ -3929,7 +3971,7 @@ class Exif(_ExifBase):
self._data.clear()
self._hidden_data.clear()
self._ifds.clear()
if data and data.startswith(b"Exif\x00\x00"):
while data and data.startswith(b"Exif\x00\x00"):
data = data[6:]
if not data:
self._info = None
@ -4006,15 +4048,19 @@ class Exif(_ExifBase):
ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag):
def get_ifd(self, tag: int) -> dict[int, Any]:
if tag not in self._ifds:
if tag == ExifTags.IFD.IFD1:
if self._info is not None and self._info.next != 0:
self._ifds[tag] = self._get_ifd_dict(self._info.next)
ifd = self._get_ifd_dict(self._info.next)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
offset = self._hidden_data.get(tag, self.get(tag))
if offset is not None:
self._ifds[tag] = self._get_ifd_dict(offset, tag)
ifd = self._get_ifd_dict(offset, tag)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif)
@ -4071,7 +4117,9 @@ class Exif(_ExifBase):
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)
camerainfo = {"ModelID": self.fp.read(4)}
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
self.fp.read(4)
# Seconds since 2000
@ -4087,17 +4135,19 @@ class Exif(_ExifBase):
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)
)[0]
self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
else:
# Interop
self._ifds[tag] = self._get_ifd_dict(tag_data, tag)
ifd = self._ifds.get(tag, {})
ifd = self._get_ifd_dict(tag_data, tag)
if ifd is not None:
self._ifds[tag] = ifd
ifd = self._ifds.setdefault(tag, {})
if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = {
k: v

View File

@ -32,7 +32,6 @@
from __future__ import annotations
import math
import numbers
import struct
from collections.abc import Sequence
from types import ModuleType
@ -160,13 +159,13 @@ class ImageDraw:
if ink is not None:
if isinstance(ink, str):
ink = ImageColor.getcolor(ink, self.mode)
if self.palette and not isinstance(ink, numbers.Number):
if self.palette and isinstance(ink, tuple):
ink = self.palette.getcolor(ink, self._image)
result_ink = self.draw.draw_ink(ink)
if fill is not None:
if isinstance(fill, str):
fill = ImageColor.getcolor(fill, self.mode)
if self.palette and not isinstance(fill, numbers.Number):
if self.palette and isinstance(fill, tuple):
fill = self.palette.getcolor(fill, self._image)
result_fill = self.draw.draw_ink(fill)
return result_ink, result_fill

View File

@ -31,14 +31,18 @@ from __future__ import annotations
import abc
import io
import itertools
import os
import struct
import sys
from typing import IO, Any, NamedTuple
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
from . import Image
from ._deprecate import deprecate
from ._util import is_path
if TYPE_CHECKING:
from ._typing import StrOrBytesPath
MAXBLOCK = 65536
SAFEBLOCK = 1024 * 1024
@ -106,32 +110,34 @@ class _Tile(NamedTuple):
class ImageFile(Image.Image):
"""Base class for image file format handlers."""
def __init__(self, fp=None, filename=None):
def __init__(
self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None
) -> None:
super().__init__()
self._min_frame = 0
self.custom_mimetype = None
self.custom_mimetype: str | None = None
self.tile = None
self.tile: list[_Tile] = []
""" A list of tile descriptors, or ``None`` """
self.readonly = 1 # until we know better
self.decoderconfig = ()
self.decoderconfig: tuple[Any, ...] = ()
self.decodermaxblock = MAXBLOCK
if is_path(fp):
# filename
self.fp = open(fp, "rb")
self.filename = fp
self.filename = os.path.realpath(os.fspath(fp))
self._exclusive_fp = True
else:
# stream
self.fp = fp
self.filename = filename
self.fp = cast(IO[bytes], fp)
self.filename = filename if filename is not None else ""
# can be overridden
self._exclusive_fp = None
self._exclusive_fp = False
try:
try:
@ -154,6 +160,9 @@ class ImageFile(Image.Image):
self.fp.close()
raise
def _open(self) -> None:
pass
def get_format_mimetype(self) -> str | None:
if self.custom_mimetype:
return self.custom_mimetype
@ -177,7 +186,7 @@ class ImageFile(Image.Image):
def load(self) -> Image.core.PixelAccess | None:
"""Load image data based on tile list"""
if self.tile is None:
if not self.tile and self._im is None:
msg = "cannot load this image"
raise OSError(msg)
@ -213,6 +222,7 @@ class ImageFile(Image.Image):
args = (args, 0, 1)
if (
decoder_name == "raw"
and isinstance(args, tuple)
and len(args) >= 3
and args[0] == self.mode
and args[0] in Image._MAPMODES
@ -312,7 +322,7 @@ class ImageFile(Image.Image):
def load_prepare(self) -> None:
# create image memory if necessary
if not self.im 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)
# create palette (optional)
if self.mode == "P":
@ -555,7 +565,7 @@ def _encode_tile(
fp: IO[bytes],
tile: list[_Tile],
bufsize: int,
fh,
fh: int | None,
exc: BaseException | None = None,
) -> None:
for encoder_name, extents, offset, args in tile:
@ -577,6 +587,7 @@ def _encode_tile(
break
else:
# slight speedup: compress to real file object
assert fh is not None
errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0:
raise _get_oserror(errcode, encoder=True) from exc
@ -722,7 +733,7 @@ class PyDecoder(PyCodec):
def pulls_fd(self) -> bool:
return self._pulls_fd
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
"""
Override to perform the decoding process.
@ -734,19 +745,22 @@ class PyDecoder(PyCodec):
msg = "unavailable in base decoder"
raise NotImplementedError(msg)
def set_as_raw(self, data: bytes, rawmode=None) -> None:
def set_as_raw(
self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = ()
) -> None:
"""
Convenience method to set the internal image from a stream of raw data
:param data: Bytes to be set
:param rawmode: The rawmode to be used for the decoder.
If not specified, it will default to the mode of the image
:param extra: Extra arguments for the decoder.
:returns: None
"""
if not rawmode:
rawmode = self.mode
d = Image._getdecoder(self.mode, "raw", rawmode)
d = Image._getdecoder(self.mode, "raw", rawmode, extra)
assert self.im is not None
d.setimage(self.im, self.state.extents())
s = d.decode(data)
@ -801,7 +815,7 @@ class PyEncoder(PyCodec):
self.fd.write(data)
return bytes_consumed, errcode
def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
def encode_to_file(self, fh: int, bufsize: int) -> int:
"""
:param fh: File handle.
:param bufsize: Buffer size.
@ -814,5 +828,5 @@ class PyEncoder(PyCodec):
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
os.write(fh, buf[status:])
return errcode

View File

@ -34,9 +34,9 @@ import warnings
from enum import IntEnum
from io import BytesIO
from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast
from . import Image
from . import Image, features
from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path
@ -98,11 +98,13 @@ class ImageFont:
def _load_pilfont(self, filename: str) -> None:
with open(filename, "rb") as fp:
image: ImageFile.ImageFile | None = None
root = os.path.splitext(filename)[0]
for ext in (".png", ".gif", ".pbm"):
if image:
image.close()
try:
fullname = os.path.splitext(filename)[0] + ext
fullname = root + ext
image = Image.open(fullname)
except Exception:
pass
@ -112,7 +114,8 @@ class ImageFont:
else:
if image:
image.close()
msg = "cannot find glyph data file"
msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
raise OSError(msg)
self.file = fullname
@ -212,7 +215,7 @@ class FreeTypeFont:
def __init__(
self,
font: StrOrBytesPath | BinaryIO | None = None,
font: StrOrBytesPath | BinaryIO,
size: float = 10,
index: int = 0,
encoding: str = "",
@ -224,7 +227,7 @@ class FreeTypeFont:
raise core.ex
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)
self.path = font
@ -232,6 +235,21 @@ class FreeTypeFont:
self.index = index
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):
layout_engine = Layout.BASIC
if core.HAVE_RAQM:
@ -245,7 +263,7 @@ class FreeTypeFont:
self.layout_engine = layout_engine
def load_from_bytes(f) -> None:
def load_from_bytes(f: IO[bytes]) -> None:
self.font_bytes = f.read()
self.font = core.getfont(
"", size, index, encoding, self.font_bytes, layout_engine
@ -267,7 +285,7 @@ class FreeTypeFont:
font, size, index, encoding, layout_engine=layout_engine
)
else:
load_from_bytes(font)
load_from_bytes(cast(IO[bytes], font))
def __getstate__(self) -> list[Any]:
return [self.path, self.size, self.index, self.encoding, self.layout_engine]
@ -769,7 +787,8 @@ class TransposedFont:
def load(filename: str) -> ImageFont:
"""
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.
:return: A font object.
@ -781,7 +800,7 @@ def load(filename: str) -> ImageFont:
def truetype(
font: StrOrBytesPath | BinaryIO | None = None,
font: StrOrBytesPath | BinaryIO,
size: float = 10,
index: int = 0,
encoding: str = "",
@ -789,9 +808,10 @@ def truetype(
) -> FreeTypeFont:
"""
Load a TrueType or OpenType font from a file or file-like object,
and create a font object.
This function loads a font object from the given file or file-like
object, and creates a font object for a font of the given size.
and create a font object. This function loads a font object from the given
file or file-like object, and creates a font object for a font of the given
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
will keep the file open as long as the FreeTypeFont object exists. Windows
@ -852,7 +872,7 @@ def truetype(
:exception ValueError: If the font size is not greater than zero.
"""
def freetype(font: StrOrBytesPath | BinaryIO | None) -> FreeTypeFont:
def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont:
return FreeTypeFont(font, size, index, encoding, layout_engine)
try:
@ -927,7 +947,10 @@ def load_path(filename: str | bytes) -> ImageFont:
return load(os.path.join(directory, filename))
except OSError:
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)

View File

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

View File

@ -19,14 +19,23 @@ from __future__ import annotations
import sys
from io import BytesIO
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Any, Callable, Union
from . import Image
from ._util import is_path
if TYPE_CHECKING:
import PyQt6
import PySide6
from . import ImageFile
QBuffer: type
QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray]
QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
qt_version: str | None
qt_versions = [
["6", "PyQt6"],
@ -37,10 +46,6 @@ qt_versions = [
qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
for version, qt_module in qt_versions:
try:
QBuffer: type
QIODevice: type
QImage: type
QPixmap: type
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice
@ -65,19 +70,20 @@ def rgb(r: int, g: int, b: int, a: int = 255) -> int:
return qRgba(r, g, b, a) & 0xFFFFFFFF
def fromqimage(im):
def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile:
"""
:param im: QImage or PIL ImageQt object
"""
buffer = QBuffer()
qt_openmode: object
if qt_version == "6":
try:
qt_openmode = QIODevice.OpenModeFlag
qt_openmode = getattr(QIODevice, "OpenModeFlag")
except AttributeError:
qt_openmode = QIODevice.OpenMode
qt_openmode = getattr(QIODevice, "OpenMode")
else:
qt_openmode = QIODevice
buffer.open(qt_openmode.ReadWrite)
buffer.open(getattr(qt_openmode, "ReadWrite"))
# preserve alpha channel with png
# otherwise ppm is more friendly with Image.open
if im.hasAlphaChannel():
@ -93,7 +99,7 @@ def fromqimage(im):
return Image.open(b)
def fromqpixmap(im) -> ImageFile.ImageFile:
def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile:
return fromqimage(im)
@ -123,7 +129,7 @@ def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
return b"".join(new_data)
def _toqclass_helper(im):
def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
data = None
colortable = None
exclusive_fp = False
@ -135,30 +141,32 @@ def _toqclass_helper(im):
if is_path(im):
im = Image.open(im)
exclusive_fp = True
assert isinstance(im, Image.Image)
qt_format = QImage.Format if qt_version == "6" else QImage
qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage
if im.mode == "1":
format = qt_format.Format_Mono
format = getattr(qt_format, "Format_Mono")
elif im.mode == "L":
format = qt_format.Format_Indexed8
format = getattr(qt_format, "Format_Indexed8")
colortable = [rgb(i, i, i) for i in range(256)]
elif im.mode == "P":
format = qt_format.Format_Indexed8
format = getattr(qt_format, "Format_Indexed8")
palette = im.getpalette()
assert palette is not None
colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
elif im.mode == "RGB":
# Populate the 4th channel with 255
im = im.convert("RGBA")
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_RGB32
format = getattr(qt_format, "Format_RGB32")
elif im.mode == "RGBA":
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_ARGB32
format = getattr(qt_format, "Format_ARGB32")
elif im.mode == "I;16":
im = im.point(lambda i: i * 256)
format = qt_format.Format_Grayscale16
format = getattr(qt_format, "Format_Grayscale16")
else:
if exclusive_fp:
im.close()
@ -174,8 +182,8 @@ def _toqclass_helper(im):
if qt_is_installed:
class ImageQt(QImage):
def __init__(self, im) -> None:
class ImageQt(QImage): # type: ignore[misc]
def __init__(self, im: Image.Image | str | QByteArray) -> None:
"""
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
class.
@ -199,10 +207,10 @@ if qt_is_installed:
self.setColorTable(im_data["colortable"])
def toqimage(im) -> ImageQt:
def toqimage(im: Image.Image | str | QByteArray) -> ImageQt:
return ImageQt(im)
def toqpixmap(im):
def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap:
qimage = toqimage(im)
return QPixmap.fromImage(qimage)
return getattr(QPixmap, "fromImage")(qimage)

View File

@ -127,10 +127,7 @@ class PhotoImage:
# palette mapped data
image.apply_transparency()
image.load()
try:
mode = image.palette.mode
except AttributeError:
mode = "RGB" # default
mode = image.palette.mode if image.palette else "RGB"
size = image.size
kw["width"], kw["height"] = size

View File

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

View File

@ -58,7 +58,7 @@ class ImtImageFile(ImageFile.ImageFile):
if s == b"\x0C":
# image data begins
self.tile = [
(
ImageFile._Tile(
"raw",
(0, 0) + self.size,
self.fp.tell() - len(buffer),

View File

@ -147,7 +147,9 @@ class IptcImageFile(ImageFile.ImageFile):
# tile
if tag == (8, 10):
self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
self.tile = [
ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
]
def load(self) -> Image.core.PixelAccess | None:
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
@ -199,9 +201,13 @@ def getiptcinfo(
data = None
info: dict[tuple[int, int], bytes | list[bytes]] = {}
if isinstance(im, IptcImageFile):
# return info dictionary right away
return im.info
for k, v in im.info.items():
if isinstance(k, tuple):
info[k] = v
return info
elif isinstance(im, JpegImagePlugin.JpegImageFile):
# extract the IPTC/NAA resource
@ -214,7 +220,7 @@ def getiptcinfo(
# as 4-byte integers, so we cannot use the get method...)
try:
data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK]
except (AttributeError, KeyError):
except KeyError:
pass
if data is None:
@ -237,4 +243,7 @@ def getiptcinfo(
except (IndexError, KeyError):
pass # expected failure
return iptc_im.info
for k, v in iptc_im.info.items():
if isinstance(k, tuple):
info[k] = v
return info

View File

@ -18,6 +18,7 @@ from __future__ import annotations
import io
import os
import struct
from collections.abc import Callable
from typing import IO, cast
from . import Image, ImageFile, ImagePalette, _binary
@ -205,7 +206,7 @@ def _parse_jp2_header(
if bitdepth > max_bitdepth:
max_bitdepth = bitdepth
if max_bitdepth <= 8:
palette = ImagePalette.ImagePalette()
palette = ImagePalette.ImagePalette("RGBA" if npc == 4 else "RGB")
for i in range(ne):
color: list[int] = []
for value in header.read_fields(">" + ("B" * npc)):
@ -286,7 +287,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
length = -1
self.tile = [
(
ImageFile._Tile(
"jpeg2k",
(0, 0) + self.size,
0,
@ -316,8 +317,13 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
else:
self.fp.seek(length - 2, os.SEEK_CUR)
@property
def reduce(self):
@property # type: ignore[override]
def reduce(
self,
) -> (
Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image]
| int
):
# https://github.com/python-pillow/Pillow/issues/4343 found that the
# new Image 'reduce' method was shadowed by this plugin's 'reduce'
# property. This attempts to allow for both scenarios
@ -338,8 +344,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
# Update the reduce and layers settings
t = self.tile[0]
assert isinstance(t[3], tuple)
t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
self.tile = [(t[0], (0, 0) + self.size, t[2], t3)]
self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)]
return ImageFile.ImageFile.load(self)

View File

@ -372,7 +372,9 @@ class JpegImageFile(ImageFile.ImageFile):
rawmode = self.mode
if self.mode == "CMYK":
rawmode = "CMYK;I" # assume adobe conventions
self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))]
self.tile = [
ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, ""))
]
# self.__offset = self.fp.tell()
break
s = self.fp.read(1)
@ -423,6 +425,7 @@ class JpegImageFile(ImageFile.ImageFile):
scale = 1
original_size = self.size
assert isinstance(a, tuple)
if a[0] == "RGB" and mode in ["L", "YCbCr"]:
self._mode = mode
a = mode, ""
@ -432,6 +435,7 @@ class JpegImageFile(ImageFile.ImageFile):
for s in [8, 4, 2, 1]:
if scale >= s:
break
assert e is not None
e = (
e[0],
e[1],
@ -441,7 +445,7 @@ class JpegImageFile(ImageFile.ImageFile):
self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s)
scale = s
self.tile = [(d, e, o, a)]
self.tile = [ImageFile._Tile(d, e, o, a)]
self.decoderconfig = (scale, 0)
box = (0, 0, original_size[0] / scale, original_size[1] / scale)
@ -747,17 +751,27 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
extra = info.get("extra", b"")
MAX_BYTES_IN_MARKER = 65533
xmp = info.get("xmp", im.info.get("xmp"))
if xmp:
overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
if len(xmp) > max_data_bytes_in_marker:
msg = "XMP data is too long"
raise ValueError(msg)
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:
ICC_OVERHEAD_LEN = 14
MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN
overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
markers = []
while icc_profile:
markers.append(icc_profile[:MAX_DATA_BYTES_IN_MARKER])
icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:]
markers.append(icc_profile[:max_data_bytes_in_marker])
icc_profile = icc_profile[max_data_bytes_in_marker:]
i = 1
for marker in markers:
size = o16(2 + ICC_OVERHEAD_LEN + len(marker))
size = o16(2 + overhead_len + len(marker))
extra += (
b"\xFF\xE2"
+ size
@ -844,7 +858,7 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
##
# Factory for making JPEG and MPO instances
def jpeg_factory(
fp: IO[bytes] | None = None, filename: str | bytes | None = None
fp: IO[bytes], filename: str | bytes | None = None
) -> JpegImageFile | MpoImageFile:
im = JpegImageFile(fp, filename)
try:

View File

@ -67,7 +67,9 @@ class McIdasImageFile(ImageFile.ImageFile):
offset = w[34] + w[15]
stride = w[15] + w[10] * w[11] * w[14]
self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))]
self.tile = [
ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))
]
# --------------------------------------------------------------------

View File

@ -26,6 +26,7 @@ from typing import IO, Any, cast
from . import (
Image,
ImageFile,
ImageSequence,
JpegImagePlugin,
TiffImagePlugin,
@ -145,7 +146,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
if self.info.get("exif") != original_exif:
self._reload_exif()
self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
self.tile = [
ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
]
self.__frame = frame
def tell(self) -> int:

View File

@ -70,9 +70,9 @@ class MspImageFile(ImageFile.ImageFile):
self._size = i16(s, 4), i16(s, 6)
if s[:4] == b"DanM":
self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
else:
self.tile = [("MSP", (0, 0) + self.size, 32, None)]
self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)]
class MspDecoder(ImageFile.PyDecoder):
@ -112,7 +112,7 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
img = io.BytesIO()
@ -152,7 +152,7 @@ class MspDecoder(ImageFile.PyDecoder):
msg = f"Corrupted MSP file in row {x}"
raise OSError(msg) from e
self.set_as_raw(img.getvalue(), ("1", 0, 1))
self.set_as_raw(img.getvalue(), "1")
return -1, 0

View File

@ -17,7 +17,7 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
from typing import IO, TYPE_CHECKING
from . import EpsImagePlugin
@ -28,15 +28,12 @@ from . import EpsImagePlugin
class PSDraw:
"""
Sets up printing to the given file. If ``fp`` is omitted,
``sys.stdout.buffer`` or ``sys.stdout`` is assumed.
``sys.stdout.buffer`` is assumed.
"""
def __init__(self, fp=None):
def __init__(self, fp: IO[bytes] | None = None) -> None:
if not fp:
try:
fp = sys.stdout.buffer
except AttributeError:
fp = sys.stdout
self.fp = fp
def begin_document(self, id: str | None = None) -> None:

View File

@ -173,6 +173,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
flags = 0
if im.mode == "P" and "custom-colormap" in im.info:
assert im.palette is not None
flags = flags & _FLAGS["custom-colormap"]
colormapsize = 4 * 256 + 2
colormapmode = im.palette.mode

Some files were not shown because too many files have changed in this diff Show More