Merge branch 'main' into imagetext
|
@ -51,10 +51,10 @@ pushd depends && ./install_webp.sh && popd
|
|||
pushd depends && ./install_imagequant.sh && popd
|
||||
|
||||
# raqm
|
||||
pushd depends && ./install_raqm.sh && popd
|
||||
pushd depends && sudo ./install_raqm.sh && popd
|
||||
|
||||
# libavif
|
||||
pushd depends && ./install_libavif.sh && popd
|
||||
pushd depends && sudo ./install_libavif.sh && popd
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==3.1.2
|
||||
cibuildwheel==3.2.1
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mypy==1.17.0
|
||||
mypy==1.18.2
|
||||
IceSpringPySideStubs-PyQt6
|
||||
IceSpringPySideStubs-PySide6
|
||||
ipython
|
||||
|
|
4
.github/workflows/docs.yml
vendored
|
@ -32,12 +32,12 @@ jobs:
|
|||
name: Docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
|
|
4
.github/workflows/lint.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
|||
name: Lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
lint-pre-commit-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
|
|
3
.github/workflows/macos-install.sh
vendored
|
@ -2,9 +2,6 @@
|
|||
|
||||
set -e
|
||||
|
||||
if [[ "$ImageOS" == "macos13" ]]; then
|
||||
brew uninstall gradle maven
|
||||
fi
|
||||
brew install \
|
||||
aom \
|
||||
dav1d \
|
||||
|
|
2
.github/workflows/stale.yml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Check issues"
|
||||
uses: actions/stale@v9
|
||||
uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "Awaiting OP Action"
|
||||
|
|
4
.github/workflows/test-docker.yml
vendored
|
@ -47,6 +47,8 @@ jobs:
|
|||
centos-stream-10-amd64,
|
||||
debian-12-bookworm-x86,
|
||||
debian-12-bookworm-amd64,
|
||||
debian-13-trixie-x86,
|
||||
debian-13-trixie-amd64,
|
||||
fedora-41-amd64,
|
||||
fedora-42-amd64,
|
||||
gentoo,
|
||||
|
@ -66,7 +68,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
2
.github/workflows/test-mingw.yml
vendored
|
@ -45,7 +45,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
2
.github/workflows/test-valgrind-memory.yml
vendored
|
@ -41,7 +41,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
12
.github/workflows/test-windows.yml
vendored
|
@ -47,19 +47,19 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/test-images
|
||||
|
@ -67,7 +67,7 @@ jobs:
|
|||
|
||||
# sets env: pythonLocation
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
@ -97,8 +97,8 @@ jobs:
|
|||
choco install nasm --no-progress
|
||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||
|
||||
choco install ghostscript --version=10.5.1 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.05.1\bin" >> $env:GITHUB_PATH
|
||||
choco install ghostscript --version=10.6.0 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.06.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
# Install extra test images
|
||||
xcopy /S /Y Tests\test-images\* Tests\images
|
||||
|
|
8
.github/workflows/test.yml
vendored
|
@ -57,7 +57,7 @@ jobs:
|
|||
- { python-version: "3.14t", disable-gil: true }
|
||||
- { python-version: "3.13t", disable-gil: true }
|
||||
# Intel
|
||||
- { os: "macos-13", python-version: "3.10" }
|
||||
- { os: "macos-15-intel", python-version: "3.10" }
|
||||
exclude:
|
||||
- { os: "macos-latest", python-version: "3.10" }
|
||||
|
||||
|
@ -65,12 +65,12 @@ jobs:
|
|||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
@ -111,7 +111,7 @@ jobs:
|
|||
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
|
||||
|
||||
- name: Register gcc problem matcher
|
||||
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
|
||||
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'"
|
||||
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
|
||||
|
||||
- name: Build
|
||||
|
|
39
.github/workflows/wheels-dependencies.sh
vendored
|
@ -93,16 +93,20 @@ ARCHIVE_SDIR=pillow-depends-main
|
|||
# Package versions for fresh source builds. Version numbers with "Patched"
|
||||
# annotations have a source code patch that is required for some platforms. If
|
||||
# you change those versions, ensure the patch is also updated.
|
||||
FREETYPE_VERSION=2.13.3
|
||||
HARFBUZZ_VERSION=11.2.1
|
||||
if [[ -n "$IOS_SDK" ]]; then
|
||||
FREETYPE_VERSION=2.13.3
|
||||
else
|
||||
FREETYPE_VERSION=2.14.1
|
||||
fi
|
||||
HARFBUZZ_VERSION=12.1.0
|
||||
LIBPNG_VERSION=1.6.50
|
||||
JPEGTURBO_VERSION=3.1.1
|
||||
OPENJPEG_VERSION=2.5.3
|
||||
JPEGTURBO_VERSION=3.1.2
|
||||
OPENJPEG_VERSION=2.5.4
|
||||
XZ_VERSION=5.8.1
|
||||
TIFF_VERSION=4.7.0
|
||||
ZSTD_VERSION=1.5.7
|
||||
TIFF_VERSION=4.7.1
|
||||
LCMS2_VERSION=2.17
|
||||
ZLIB_VERSION=1.3.1
|
||||
ZLIB_NG_VERSION=2.2.4
|
||||
ZLIB_NG_VERSION=2.2.5
|
||||
LIBWEBP_VERSION=1.6.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.17.0
|
||||
|
@ -165,7 +169,7 @@ function build_brotli {
|
|||
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_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib $HOST_CMAKE_FLAGS . \
|
||||
&& make install)
|
||||
&& make -j4 install)
|
||||
touch brotli-stamp
|
||||
}
|
||||
|
||||
|
@ -249,18 +253,26 @@ function build_libavif {
|
|||
cp $WORKDIR/meson-cross.txt $out_dir/crossfile-apple.meson
|
||||
fi
|
||||
|
||||
(cd $out_dir && make install)
|
||||
(cd $out_dir && make -j4 install)
|
||||
|
||||
touch libavif-stamp
|
||||
}
|
||||
|
||||
function build_zstd {
|
||||
if [ -e zstd-stamp ]; then return; fi
|
||||
local out_dir=$(fetch_unpack https://github.com/facebook/zstd/releases/download/v$ZSTD_VERSION/zstd-$ZSTD_VERSION.tar.gz)
|
||||
(cd $out_dir \
|
||||
&& make -j4 install)
|
||||
touch zstd-stamp
|
||||
}
|
||||
|
||||
function build {
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||
yum remove -y zlib-devel
|
||||
fi
|
||||
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
|
||||
build_new_zlib
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
CFLAGS="$CFLAGS -headerpad_max_install_names" build_zlib_ng
|
||||
else
|
||||
build_zlib_ng
|
||||
fi
|
||||
|
@ -285,6 +297,7 @@ function build {
|
|||
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
|
||||
--disable-webp --disable-libdeflate --disable-zstd
|
||||
else
|
||||
build_zstd
|
||||
build_tiff
|
||||
fi
|
||||
|
||||
|
@ -309,6 +322,10 @@ function build {
|
|||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# Custom freetype build
|
||||
if [[ -z "$IOS_SDK" ]]; then
|
||||
build_simple sed 4.9 https://mirrors.middlendian.com/gnu/sed
|
||||
fi
|
||||
|
||||
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
|
||||
else
|
||||
build_freetype
|
||||
|
|
59
.github/workflows/wheels.yml
vendored
|
@ -39,6 +39,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
EXPECTED_DISTS: 91
|
||||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
|
@ -52,21 +53,21 @@ jobs:
|
|||
include:
|
||||
- name: "macOS 10.10 x86_64"
|
||||
platform: macos
|
||||
os: macos-13
|
||||
os: macos-15-intel
|
||||
cibw_arch: x86_64
|
||||
build: "cp3{9,10,11}*"
|
||||
build: "cp3{10,11}*"
|
||||
macosx_deployment_target: "10.10"
|
||||
- name: "macOS 10.13 x86_64"
|
||||
platform: macos
|
||||
os: macos-13
|
||||
os: macos-15-intel
|
||||
cibw_arch: x86_64
|
||||
build: "cp3{12,13,14}*"
|
||||
build: "cp3{12,13}*"
|
||||
macosx_deployment_target: "10.13"
|
||||
- name: "macOS 10.15 x86_64"
|
||||
platform: macos
|
||||
os: macos-13
|
||||
os: macos-15-intel
|
||||
cibw_arch: x86_64
|
||||
build: "pp3*"
|
||||
build: "{cp314,pp3}*"
|
||||
macosx_deployment_target: "10.15"
|
||||
- name: "macOS arm64"
|
||||
platform: macos
|
||||
|
@ -103,15 +104,15 @@ jobs:
|
|||
cibw_arch: arm64_iphonesimulator
|
||||
- name: "iOS x86_64 simulator"
|
||||
platform: ios
|
||||
os: macos-13
|
||||
os: macos-15-intel
|
||||
cibw_arch: x86_64_iphonesimulator
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
|
@ -153,18 +154,18 @@ jobs:
|
|||
- cibw_arch: ARM64
|
||||
os: windows-11-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: python-pillow/test-images
|
||||
path: Tests\test-images
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
|
@ -231,15 +232,15 @@ jobs:
|
|||
path: winbuild\build\bin\fribidi*
|
||||
|
||||
sdist:
|
||||
if: github.event_name != 'schedule'
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
|
@ -250,15 +251,33 @@ jobs:
|
|||
name: dist-sdist
|
||||
path: dist/*.tar.gz
|
||||
|
||||
count-dists:
|
||||
needs: [build-native-wheels, windows, sdist]
|
||||
runs-on: ubuntu-latest
|
||||
name: Count dists
|
||||
steps:
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: "What did we get?"
|
||||
run: |
|
||||
ls -alR
|
||||
echo "Number of dists, should be $EXPECTED_DISTS:"
|
||||
files=$(ls dist 2>/dev/null | wc -l)
|
||||
echo $files
|
||||
[ "$files" -eq $EXPECTED_DISTS ] || exit 1
|
||||
|
||||
scientific-python-nightly-wheels-publish:
|
||||
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
needs: [build-native-wheels, windows]
|
||||
needs: count-dists
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload wheels to scientific-python-nightly-wheels
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: dist-*
|
||||
pattern: dist-!(sdist)*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Upload wheels to scientific-python-nightly-wheels
|
||||
|
@ -269,7 +288,7 @@ jobs:
|
|||
|
||||
pypi-publish:
|
||||
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
needs: [build-native-wheels, windows, sdist]
|
||||
needs: count-dists
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload release to PyPI
|
||||
environment:
|
||||
|
@ -278,7 +297,7 @@ jobs:
|
|||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: dist-*
|
||||
path: dist
|
||||
|
|
2
.github/zizmor.yml
vendored
|
@ -1,5 +1,5 @@
|
|||
# Configuration for the zizmor static analysis tool, run via pre-commit in CI
|
||||
# https://woodruffw.github.io/zizmor/configuration/
|
||||
# https://docs.zizmor.sh/configuration/
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.2
|
||||
rev: v0.13.3
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 25.1.0
|
||||
rev: 25.9.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
|
@ -24,7 +24,7 @@ repos:
|
|||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$|\.patch$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v20.1.7
|
||||
rev: v21.1.2
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
|
@ -36,7 +36,7 @@ repos:
|
|||
- id: rst-backticks
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
|
@ -51,14 +51,14 @@ repos:
|
|||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/|\.patch$
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.33.2
|
||||
rev: 0.34.0
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.11.0
|
||||
- repo: https://github.com/zizmorcore/zizmor-pre-commit
|
||||
rev: v1.14.2
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
|
@ -68,7 +68,7 @@ repos:
|
|||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.6.0
|
||||
rev: v2.7.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
|
@ -79,7 +79,7 @@ repos:
|
|||
additional_dependencies: [trove-classifiers>=2024.10.12]
|
||||
|
||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||
rev: 1.5.0
|
||||
rev: 1.6.0
|
||||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
|
|
1
Tests/createfontdatachunk.py
Executable file → Normal file
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
|
|
|
@ -175,6 +175,14 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
|
|||
return pytest.mark.skipif(not features.check(feature), reason=reason)
|
||||
|
||||
|
||||
def has_feature_version(feature: str, required: str) -> bool:
|
||||
version = features.version(feature)
|
||||
assert version is not None
|
||||
version_required = parse_version(required)
|
||||
version_available = parse_version(version)
|
||||
return version_available >= version_required
|
||||
|
||||
|
||||
def skip_unless_feature_version(
|
||||
feature: str, required: str, reason: str | None = None
|
||||
) -> pytest.MarkDecorator:
|
||||
|
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.5 KiB |
BIN
Tests/images/colr_bungee_older.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
Tests/images/frame_size.mpo
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 117 KiB |
275
Tests/test_arro3.py
Normal file
|
@ -0,0 +1,275 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import (
|
||||
assert_deep_equal,
|
||||
assert_image_equal,
|
||||
hopper,
|
||||
is_big_endian,
|
||||
)
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from arro3 import compute # type: ignore [import-not-found]
|
||||
from arro3.core import ( # type: ignore [import-not-found]
|
||||
Array,
|
||||
DataType,
|
||||
Field,
|
||||
fixed_size_list_array,
|
||||
)
|
||||
else:
|
||||
arro3 = pytest.importorskip("arro3", reason="Arro3 not installed")
|
||||
from arro3 import compute
|
||||
from arro3.core import Array, DataType, Field, fixed_size_list_array
|
||||
|
||||
TEST_IMAGE_SIZE = (10, 10)
|
||||
|
||||
|
||||
def _test_img_equals_pyarray(
|
||||
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
|
||||
) -> None:
|
||||
assert img.height * img.width * elts_per_pixel == len(arr)
|
||||
px = img.load()
|
||||
assert px is not None
|
||||
if elts_per_pixel > 1 and mask is None:
|
||||
# have to do element-wise comparison when we're comparing
|
||||
# flattened r,g,b,a to a pixel.
|
||||
mask = list(range(elts_per_pixel))
|
||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||
if mask:
|
||||
pixel = px[x, y]
|
||||
assert isinstance(pixel, tuple)
|
||||
for ix, elt in enumerate(mask):
|
||||
if elts_per_pixel == 1:
|
||||
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
|
||||
else:
|
||||
assert (
|
||||
pixel[ix]
|
||||
== arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
|
||||
)
|
||||
else:
|
||||
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
|
||||
|
||||
|
||||
def _test_img_equals_int32_pyarray(
|
||||
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
|
||||
) -> None:
|
||||
assert img.height * img.width * elts_per_pixel == len(arr)
|
||||
px = img.load()
|
||||
assert px is not None
|
||||
if mask is None:
|
||||
# have to do element-wise comparison when we're comparing
|
||||
# flattened rgba in an uint32 to a pixel.
|
||||
mask = list(range(elts_per_pixel))
|
||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||
pixel = px[x, y]
|
||||
assert isinstance(pixel, tuple)
|
||||
arr_pixel_int = arr[y * img.width + x].as_py()
|
||||
arr_pixel_tuple = (
|
||||
arr_pixel_int % 256,
|
||||
(arr_pixel_int // 256) % 256,
|
||||
(arr_pixel_int // 256**2) % 256,
|
||||
(arr_pixel_int // 256**3),
|
||||
)
|
||||
if is_big_endian():
|
||||
arr_pixel_tuple = arr_pixel_tuple[::-1]
|
||||
|
||||
for ix, elt in enumerate(mask):
|
||||
assert pixel[ix] == arr_pixel_tuple[elt]
|
||||
|
||||
|
||||
fl_uint8_4_type = DataType.list(Field("_", DataType.uint8()).with_nullable(False), 4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, dtype, mask",
|
||||
(
|
||||
("L", DataType.uint8(), None),
|
||||
("I", DataType.int32(), None),
|
||||
("F", DataType.float32(), None),
|
||||
("LA", fl_uint8_4_type, [0, 3]),
|
||||
("RGB", fl_uint8_4_type, [0, 1, 2]),
|
||||
("RGBA", fl_uint8_4_type, None),
|
||||
("RGBX", fl_uint8_4_type, None),
|
||||
("CMYK", fl_uint8_4_type, None),
|
||||
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
|
||||
("HSV", fl_uint8_4_type, [0, 1, 2]),
|
||||
),
|
||||
)
|
||||
def test_to_array(mode: str, dtype: DataType, mask: list[int] | None) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
# Resize to non-square
|
||||
img = img.crop((3, 0, 124, 127))
|
||||
assert img.size == (121, 127)
|
||||
|
||||
arr = Array(img)
|
||||
_test_img_equals_pyarray(img, arr, mask)
|
||||
assert arr.type == dtype
|
||||
|
||||
reloaded = Image.fromarrow(arr, mode, img.size)
|
||||
assert_image_equal(img, reloaded)
|
||||
|
||||
|
||||
def test_lifetime() -> None:
|
||||
# valgrind shouldn't error out here.
|
||||
# arrays should be accessible after the image is deleted.
|
||||
|
||||
img = hopper("L")
|
||||
|
||||
arr_1 = Array(img)
|
||||
arr_2 = Array(img)
|
||||
|
||||
del img
|
||||
|
||||
assert compute.sum(arr_1).as_py() > 0
|
||||
del arr_1
|
||||
|
||||
assert compute.sum(arr_2).as_py() > 0
|
||||
del arr_2
|
||||
|
||||
|
||||
def test_lifetime2() -> None:
|
||||
# valgrind shouldn't error out here.
|
||||
# img should remain after the arrays are collected.
|
||||
|
||||
img = hopper("L")
|
||||
|
||||
arr_1 = Array(img)
|
||||
arr_2 = Array(img)
|
||||
|
||||
assert compute.sum(arr_1).as_py() > 0
|
||||
del arr_1
|
||||
|
||||
assert compute.sum(arr_2).as_py() > 0
|
||||
del arr_2
|
||||
|
||||
img2 = img.copy()
|
||||
px = img2.load()
|
||||
assert px # make mypy happy
|
||||
assert isinstance(px[0, 0], int)
|
||||
|
||||
|
||||
class DataShape(NamedTuple):
|
||||
dtype: DataType
|
||||
# Strictly speaking, elt should be a pixel or pixel component, so
|
||||
# list[uint8][4], float, int, uint32, uint8, etc. But more
|
||||
# correctly, it should be exactly the dtype from the line above.
|
||||
elt: Any
|
||||
elts_per_pixel: int
|
||||
|
||||
|
||||
UINT_ARR = DataShape(
|
||||
dtype=fl_uint8_4_type,
|
||||
elt=[1, 2, 3, 4], # array of 4 uint8 per pixel
|
||||
elts_per_pixel=1, # only one array per pixel
|
||||
)
|
||||
|
||||
UINT = DataShape(
|
||||
dtype=DataType.uint8(),
|
||||
elt=3, # one uint8,
|
||||
elts_per_pixel=4, # but repeated 4x per pixel
|
||||
)
|
||||
|
||||
UINT32 = DataShape(
|
||||
dtype=DataType.uint32(),
|
||||
elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000
|
||||
elts_per_pixel=1, # one per pixel
|
||||
)
|
||||
|
||||
INT32 = DataShape(
|
||||
dtype=DataType.uint32(),
|
||||
elt=0x12CDEF45, # one packed int
|
||||
elts_per_pixel=1, # one per pixel
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, data_tp, mask",
|
||||
(
|
||||
("L", DataShape(DataType.uint8(), 3, 1), None),
|
||||
("I", DataShape(DataType.int32(), 1 << 24, 1), None),
|
||||
("F", DataShape(DataType.float32(), 3.14159, 1), None),
|
||||
("LA", UINT_ARR, [0, 3]),
|
||||
("LA", UINT, [0, 3]),
|
||||
("RGB", UINT_ARR, [0, 1, 2]),
|
||||
("RGBA", UINT_ARR, None),
|
||||
("CMYK", UINT_ARR, None),
|
||||
("YCbCr", UINT_ARR, [0, 1, 2]),
|
||||
("HSV", UINT_ARR, [0, 1, 2]),
|
||||
("RGB", UINT, [0, 1, 2]),
|
||||
("RGBA", UINT, None),
|
||||
("CMYK", UINT, None),
|
||||
("YCbCr", UINT, [0, 1, 2]),
|
||||
("HSV", UINT, [0, 1, 2]),
|
||||
),
|
||||
)
|
||||
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
|
||||
(dtype, elt, elts_per_pixel) = data_tp
|
||||
|
||||
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||
if dtype == fl_uint8_4_type:
|
||||
tmp_arr = Array(elt * (ct_pixels * elts_per_pixel), type=DataType.uint8())
|
||||
arr = fixed_size_list_array(tmp_arr, 4)
|
||||
else:
|
||||
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
|
||||
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
|
||||
|
||||
_test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, mask",
|
||||
(
|
||||
("LA", [0, 3]),
|
||||
("RGB", [0, 1, 2]),
|
||||
("RGBA", None),
|
||||
("CMYK", None),
|
||||
("YCbCr", [0, 1, 2]),
|
||||
("HSV", [0, 1, 2]),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
|
||||
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
|
||||
(dtype, elt, elts_per_pixel) = data_tp
|
||||
|
||||
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||
arr = Array([elt] * (ct_pixels * elts_per_pixel), type=dtype)
|
||||
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
|
||||
|
||||
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, metadata",
|
||||
(
|
||||
("LA", ["L", "X", "X", "A"]),
|
||||
("RGB", ["R", "G", "B", "X"]),
|
||||
("RGBX", ["R", "G", "B", "X"]),
|
||||
("RGBA", ["R", "G", "B", "A"]),
|
||||
("CMYK", ["C", "M", "Y", "K"]),
|
||||
("YCbCr", ["Y", "Cb", "Cr", "X"]),
|
||||
("HSV", ["H", "S", "V", "X"]),
|
||||
),
|
||||
)
|
||||
def test_image_metadata(mode: str, metadata: list[str]) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
arr = Array(img)
|
||||
|
||||
assert arr.type.value_field
|
||||
assert arr.type.value_field.metadata
|
||||
assert arr.type.value_field.metadata[b"image"]
|
||||
|
||||
parsed_metadata = json.loads(arr.type.value_field.metadata[b"image"].decode("utf8"))
|
||||
|
||||
assert "bands" in parsed_metadata
|
||||
assert parsed_metadata["bands"] == metadata
|
|
@ -14,6 +14,7 @@ import pytest
|
|||
|
||||
from PIL import (
|
||||
AvifImagePlugin,
|
||||
GifImagePlugin,
|
||||
Image,
|
||||
ImageDraw,
|
||||
ImageFile,
|
||||
|
@ -220,6 +221,7 @@ class TestFileAvif:
|
|||
def test_background_from_gif(self, tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
original_value = im.convert("RGB").getpixel((1, 1))
|
||||
assert isinstance(original_value, tuple)
|
||||
|
||||
# Save as AVIF
|
||||
out_avif = tmp_path / "temp.avif"
|
||||
|
@ -232,6 +234,7 @@ class TestFileAvif:
|
|||
|
||||
with Image.open(out_gif) as reread:
|
||||
reread_value = reread.convert("RGB").getpixel((1, 1))
|
||||
assert isinstance(reread_value, tuple)
|
||||
difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)])
|
||||
assert difference <= 6
|
||||
|
||||
|
@ -240,6 +243,7 @@ class TestFileAvif:
|
|||
with Image.open("Tests/images/chi.gif") as im:
|
||||
im.save(temp_file)
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, AvifImagePlugin.AvifImageFile)
|
||||
assert im.n_frames == 1
|
||||
|
||||
def test_invalid_file(self) -> None:
|
||||
|
@ -598,10 +602,12 @@ class TestAvifAnimation:
|
|||
"""
|
||||
|
||||
with Image.open(TEST_AVIF_FILE) as im:
|
||||
assert isinstance(im, AvifImagePlugin.AvifImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/avif/star.avifs") as im:
|
||||
assert isinstance(im, AvifImagePlugin.AvifImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
|
@ -612,11 +618,13 @@ class TestAvifAnimation:
|
|||
"""
|
||||
|
||||
with Image.open("Tests/images/avif/star.gif") as original:
|
||||
assert isinstance(original, GifImagePlugin.GifImageFile)
|
||||
assert original.n_frames > 1
|
||||
|
||||
temp_file = tmp_path / "temp.avif"
|
||||
original.save(temp_file, save_all=True)
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, AvifImagePlugin.AvifImageFile)
|
||||
assert im.n_frames == original.n_frames
|
||||
|
||||
# Compare first frame in P mode to frame from original GIF
|
||||
|
@ -636,6 +644,7 @@ class TestAvifAnimation:
|
|||
|
||||
def check(temp_file: Path) -> None:
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, AvifImagePlugin.AvifImageFile)
|
||||
assert im.n_frames == 4
|
||||
|
||||
# Compare first frame to original
|
||||
|
@ -708,6 +717,7 @@ class TestAvifAnimation:
|
|||
)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, AvifImagePlugin.AvifImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
|
@ -737,6 +747,7 @@ class TestAvifAnimation:
|
|||
)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, AvifImagePlugin.AvifImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
from PIL import BmpImagePlugin, Image, _binary
|
||||
from PIL._binary import o16le as o16
|
||||
from PIL._binary import o32le as o32
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -114,7 +116,7 @@ def test_save_float_dpi(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_load_dib() -> None:
|
||||
# test for #1293, Imagegrab returning Unsupported Bitfields Format
|
||||
# test for #1293, ImageGrab returning Unsupported Bitfields Format
|
||||
with Image.open("Tests/images/clipboard.dib") as im:
|
||||
assert im.format == "DIB"
|
||||
assert im.get_format_mimetype() == "image/bmp"
|
||||
|
@ -219,6 +221,18 @@ def test_rle8_eof(file_name: str, length: int) -> None:
|
|||
im.load()
|
||||
|
||||
|
||||
def test_unsupported_bmp_bitfields_layout() -> None:
|
||||
fp = io.BytesIO(
|
||||
o32(40) # header size
|
||||
+ b"\x00" * 10
|
||||
+ o16(1) # bits
|
||||
+ o32(3) # BITFIELDS compression
|
||||
+ b"\x00" * 32
|
||||
)
|
||||
with pytest.raises(OSError, match="Unsupported BMP bitfields layout"):
|
||||
Image.open(fp)
|
||||
|
||||
|
||||
def test_offset() -> None:
|
||||
# This image has been hexedited
|
||||
# to exclude the palette size from the pixel data offset
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import CurImagePlugin, Image
|
||||
from PIL._binary import o8
|
||||
from PIL._binary import o16le as o16
|
||||
from PIL._binary import o32le as o32
|
||||
|
||||
TEST_FILE = "Tests/images/deerstalker.cur"
|
||||
|
||||
|
@ -17,6 +22,24 @@ def test_sanity() -> None:
|
|||
assert im.getpixel((16, 16)) == (84, 87, 86, 255)
|
||||
|
||||
|
||||
def test_largest_cursor() -> None:
|
||||
magic = b"\x00\x00\x02\x00"
|
||||
sizes = ((1, 1), (8, 8), (4, 4))
|
||||
data = magic + o16(len(sizes))
|
||||
for w, h in sizes:
|
||||
image_offset = 6 + len(sizes) * 16 if (w, h) == max(sizes) else 0
|
||||
data += o8(w) + o8(h) + o8(0) * 10 + o32(image_offset)
|
||||
data += (
|
||||
o32(12) # header size
|
||||
+ o16(8) # width
|
||||
+ o16(16) # height
|
||||
+ o16(0) # planes
|
||||
+ o16(1) # bits
|
||||
)
|
||||
with Image.open(BytesIO(data)) as im:
|
||||
assert im.size == (8, 8)
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
|
@ -26,6 +49,7 @@ def test_invalid_file() -> None:
|
|||
no_cursors_file = "Tests/images/no_cursors.cur"
|
||||
|
||||
cur = CurImagePlugin.CurImageFile(TEST_FILE)
|
||||
assert cur.fp is not None
|
||||
cur.fp.close()
|
||||
with open(no_cursors_file, "rb") as cur.fp:
|
||||
with pytest.raises(TypeError):
|
||||
|
|
|
@ -197,6 +197,14 @@ def test_load_long_binary_data(prefix: bytes) -> None:
|
|||
assert img.format == "EPS"
|
||||
|
||||
|
||||
def test_begin_binary() -> None:
|
||||
with open("Tests/images/eps/binary_preview_map.eps", "rb") as fp:
|
||||
data = bytearray(fp.read())
|
||||
data[76875 : 76875 + 11] = b"%" * 11
|
||||
with Image.open(io.BytesIO(data)) as img:
|
||||
assert img.size == (399, 480)
|
||||
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
|
|
@ -48,6 +48,7 @@ def test_sanity() -> None:
|
|||
def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
with Image.open(animated_test_file_with_prefix_chunk) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
assert im.mode == "P"
|
||||
assert im.size == (320, 200)
|
||||
assert im.format == "FLI"
|
||||
|
@ -55,6 +56,7 @@ def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
assert im.is_animated
|
||||
|
||||
palette = im.getpalette()
|
||||
assert palette is not None
|
||||
assert palette[3:6] == [255, 255, 255]
|
||||
assert palette[381:384] == [204, 204, 12]
|
||||
assert palette[765:] == [252, 0, 0]
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import GbrImagePlugin, Image
|
||||
from PIL import GbrImagePlugin, Image, _binary
|
||||
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
||||
|
@ -31,8 +33,49 @@ def test_multiple_load_operations() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/gbr.png")
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
def create_gbr_image(info: dict[str, int] = {}, magic_number=b"") -> BytesIO:
|
||||
return BytesIO(
|
||||
b"".join(
|
||||
_binary.o32be(i)
|
||||
for i in [
|
||||
info.get("header_size", 20),
|
||||
info.get("version", 1),
|
||||
info.get("width", 1),
|
||||
info.get("height", 1),
|
||||
info.get("color_depth", 1),
|
||||
]
|
||||
)
|
||||
+ magic_number
|
||||
)
|
||||
|
||||
with pytest.raises(SyntaxError):
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
for f in [
|
||||
create_gbr_image({"header_size": 0}),
|
||||
create_gbr_image({"width": 0}),
|
||||
create_gbr_image({"height": 0}),
|
||||
]:
|
||||
with pytest.raises(SyntaxError, match="not a GIMP brush"):
|
||||
GbrImagePlugin.GbrImageFile(f)
|
||||
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
with pytest.raises(SyntaxError, match="Unsupported GIMP brush version"):
|
||||
GbrImagePlugin.GbrImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_unsupported_gimp_brush() -> None:
|
||||
f = create_gbr_image({"color_depth": 2})
|
||||
with pytest.raises(SyntaxError, match="Unsupported GIMP brush color depth: 2"):
|
||||
GbrImagePlugin.GbrImageFile(f)
|
||||
|
||||
|
||||
def test_bad_magic_number() -> None:
|
||||
f = create_gbr_image({"version": 2}, magic_number=b"badm")
|
||||
with pytest.raises(SyntaxError, match="not a GIMP brush, bad magic number"):
|
||||
GbrImagePlugin.GbrImageFile(f)
|
||||
|
||||
|
||||
def test_L() -> None:
|
||||
f = create_gbr_image()
|
||||
with Image.open(f) as im:
|
||||
assert im.mode == "L"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import GdImageFile, UnidentifiedImageError
|
||||
|
@ -16,6 +18,14 @@ def test_sanity() -> None:
|
|||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
|
||||
|
||||
|
||||
def test_transparency() -> None:
|
||||
with open(TEST_GD_FILE, "rb") as fp:
|
||||
data = bytearray(fp.read())
|
||||
data[7:11] = b"\x00\x00\x00\x05"
|
||||
with GdImageFile.open(BytesIO(data)) as im:
|
||||
assert im.info["transparency"] == 5
|
||||
|
||||
|
||||
def test_bad_mode() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
GdImageFile.open(TEST_GD_FILE, "bad mode")
|
||||
|
|
|
@ -293,6 +293,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 5
|
||||
|
||||
|
||||
|
@ -1374,6 +1375,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
|
|||
|
||||
with Image.open(out) as im:
|
||||
# Assert that the frames are correct, and each frame has the same palette
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
|
||||
assert im.palette is not None
|
||||
assert im.global_palette is not None
|
||||
|
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
|||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, IptcImagePlugin, TiffImagePlugin, TiffTags
|
||||
|
||||
from .helper import assert_image_equal, hopper
|
||||
|
@ -9,21 +11,78 @@ from .helper import assert_image_equal, hopper
|
|||
TEST_FILE = "Tests/images/iptc.jpg"
|
||||
|
||||
|
||||
def create_iptc_image(info: dict[str, int] = {}) -> BytesIO:
|
||||
def field(tag, value):
|
||||
return bytes((0x1C,) + tag + (0, len(value))) + value
|
||||
|
||||
data = field((3, 60), bytes((info.get("layers", 1), info.get("component", 0))))
|
||||
data += field((3, 120), bytes((info.get("compression", 1),)))
|
||||
if "band" in info:
|
||||
data += field((3, 65), bytes((info["band"] + 1,)))
|
||||
data += field((3, 20), b"\x01") # width
|
||||
data += field((3, 30), b"\x01") # height
|
||||
data += field(
|
||||
(8, 10),
|
||||
bytes((info.get("data", 0),)),
|
||||
)
|
||||
|
||||
return BytesIO(data)
|
||||
|
||||
|
||||
def test_open() -> None:
|
||||
expected = Image.new("L", (1, 1))
|
||||
|
||||
f = BytesIO(
|
||||
b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01"
|
||||
b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00"
|
||||
)
|
||||
f = create_iptc_image()
|
||||
with Image.open(f) as im:
|
||||
assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")]
|
||||
assert im.tile == [("iptc", (0, 0, 1, 1), 25, ("raw", None))]
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
with Image.open(f) as im:
|
||||
assert im.load() is not None
|
||||
|
||||
|
||||
def test_field_length() -> None:
|
||||
f = create_iptc_image()
|
||||
f.seek(28)
|
||||
f.write(b"\xff")
|
||||
with pytest.raises(OSError, match="illegal field length in IPTC/NAA file"):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layers, mode", ((3, "RGB"), (4, "CMYK")))
|
||||
def test_layers(layers: int, mode: str) -> None:
|
||||
for band in range(-1, layers):
|
||||
info = {"layers": layers, "component": 1, "data": 5}
|
||||
if band != -1:
|
||||
info["band"] = band
|
||||
f = create_iptc_image(info)
|
||||
with Image.open(f) as im:
|
||||
assert im.mode == mode
|
||||
|
||||
data = [0] * layers
|
||||
data[max(band, 0)] = 5
|
||||
assert im.getpixel((0, 0)) == tuple(data)
|
||||
|
||||
|
||||
def test_unknown_compression() -> None:
|
||||
f = create_iptc_image({"compression": 2})
|
||||
with pytest.raises(OSError, match="Unknown IPTC image compression"):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
||||
def test_getiptcinfo() -> None:
|
||||
f = create_iptc_image()
|
||||
with Image.open(f) as im:
|
||||
assert IptcImagePlugin.getiptcinfo(im) == {
|
||||
(3, 60): b"\x01\x00",
|
||||
(3, 120): b"\x01",
|
||||
(3, 20): b"\x01",
|
||||
(3, 30): b"\x01",
|
||||
}
|
||||
|
||||
|
||||
def test_getiptcinfo_jpg_none() -> None:
|
||||
# Arrange
|
||||
with hopper() as im:
|
||||
|
|
|
@ -330,8 +330,10 @@ class TestFileJpeg:
|
|||
|
||||
# Reading
|
||||
with Image.open("Tests/images/exif_gps.jpg") as im:
|
||||
exif = im._getexif()
|
||||
assert exif[gps_index] == expected_exif_gps
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
exif_data = im._getexif()
|
||||
assert exif_data is not None
|
||||
assert exif_data[gps_index] == expected_exif_gps
|
||||
|
||||
# Writing
|
||||
f = tmp_path / "temp.jpg"
|
||||
|
@ -340,8 +342,10 @@ class TestFileJpeg:
|
|||
hopper().save(f, exif=exif)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
exif = reloaded._getexif()
|
||||
assert exif[gps_index] == expected_exif_gps
|
||||
assert isinstance(reloaded, JpegImagePlugin.JpegImageFile)
|
||||
exif_data = reloaded._getexif()
|
||||
assert exif_data is not None
|
||||
assert exif_data[gps_index] == expected_exif_gps
|
||||
|
||||
def test_empty_exif_gps(self) -> None:
|
||||
with Image.open("Tests/images/empty_gps_ifd.jpg") as im:
|
||||
|
@ -368,6 +372,7 @@ class TestFileJpeg:
|
|||
exifs = []
|
||||
for i in range(2):
|
||||
with Image.open("Tests/images/exif-200dpcm.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
exifs.append(im._getexif())
|
||||
assert exifs[0] == exifs[1]
|
||||
|
||||
|
@ -401,13 +406,17 @@ class TestFileJpeg:
|
|||
}
|
||||
|
||||
with Image.open("Tests/images/exif_gps.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
exif = im._getexif()
|
||||
assert exif is not None
|
||||
|
||||
for tag, value in expected_exif.items():
|
||||
assert value == exif[tag]
|
||||
|
||||
def test_exif_gps_typeerror(self) -> None:
|
||||
with Image.open("Tests/images/exif_gps_typeerror.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
|
||||
# Should not raise a TypeError
|
||||
im._getexif()
|
||||
|
||||
|
@ -487,7 +496,9 @@ class TestFileJpeg:
|
|||
|
||||
def test_exif(self) -> None:
|
||||
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
info = im._getexif()
|
||||
assert info is not None
|
||||
assert info[305] == "Adobe Photoshop CS Macintosh"
|
||||
|
||||
def test_get_child_images(self) -> None:
|
||||
|
@ -690,11 +701,13 @@ class TestFileJpeg:
|
|||
|
||||
def test_save_multiple_16bit_qtables(self) -> None:
|
||||
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
im2 = self.roundtrip(im, qtables="keep")
|
||||
assert im.quantization == im2.quantization
|
||||
|
||||
def test_save_single_16bit_qtable(self) -> None:
|
||||
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
im2 = self.roundtrip(im, qtables={0: im.quantization[0]})
|
||||
assert len(im2.quantization) == 1
|
||||
assert im2.quantization[0] == im.quantization[0]
|
||||
|
@ -898,7 +911,10 @@ class TestFileJpeg:
|
|||
# in contrast to normal 8
|
||||
with Image.open("Tests/images/exif-ifd-offset.jpg") as im:
|
||||
# Act / Assert
|
||||
assert im._getexif()[306] == "2017:03:13 23:03:09"
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
exif = im._getexif()
|
||||
assert exif is not None
|
||||
assert exif[306] == "2017:03:13 23:03:09"
|
||||
|
||||
def test_multiple_exif(self) -> None:
|
||||
with Image.open("Tests/images/multiple_exif.jpg") as im:
|
||||
|
|
|
@ -355,6 +355,27 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Should not segfault
|
||||
im.save(outfile)
|
||||
|
||||
def test_whitepoint_tag(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
out = tmp_path / "temp.tif"
|
||||
hopper().save(out, tiffinfo={318: (0.3127, 0.3289)})
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289))
|
||||
|
||||
# Save tag by default
|
||||
out = tmp_path / "temp2.tif"
|
||||
with Image.open("Tests/images/rdf.tif") as im:
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[318] == pytest.approx((0.3127, 0.3289999))
|
||||
|
||||
def test_xmlpacket_tag(
|
||||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
|
@ -365,8 +386,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
if 700 in reloaded.tag_v2:
|
||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||
|
||||
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
# issue #1765
|
||||
|
|
|
@ -104,25 +104,27 @@ def test_exif(test_file: str) -> None:
|
|||
|
||||
|
||||
def test_frame_size() -> None:
|
||||
# This image has been hexedited to contain a different size
|
||||
# in the SOF marker of the second frame
|
||||
with Image.open("Tests/images/sugarshack_frame_size.mpo") as im:
|
||||
assert im.size == (640, 480)
|
||||
with Image.open("Tests/images/frame_size.mpo") as im:
|
||||
assert im.size == (56, 70)
|
||||
im.load()
|
||||
|
||||
im.seek(1)
|
||||
assert im.size == (680, 480)
|
||||
assert im.size == (349, 434)
|
||||
im.load()
|
||||
|
||||
im.seek(0)
|
||||
assert im.size == (640, 480)
|
||||
assert im.size == (56, 70)
|
||||
|
||||
|
||||
def test_ignore_frame_size() -> None:
|
||||
# Ignore the different size of the second frame
|
||||
# since this is not a "Large Thumbnail" image
|
||||
with Image.open("Tests/images/ignore_frame_size.mpo") as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
assert im.size == (64, 64)
|
||||
|
||||
im.seek(1)
|
||||
assert im.mpinfo is not None
|
||||
assert (
|
||||
im.mpinfo[0xB002][1]["Attribute"]["MPType"]
|
||||
== "Multi-Frame Image: (Disparity)"
|
||||
|
@ -155,6 +157,7 @@ def test_reload_exif_after_seek() -> None:
|
|||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_mp(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
mpinfo = im._getmp()
|
||||
assert mpinfo is not None
|
||||
assert mpinfo[45056] == b"0100"
|
||||
|
@ -165,6 +168,7 @@ def test_mp_offset() -> None:
|
|||
# This image has been manually hexedited to have an IFD offset of 10
|
||||
# in APP2 data, in contrast to normal 8
|
||||
with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
mpinfo = im._getmp()
|
||||
assert mpinfo is not None
|
||||
assert mpinfo[45056] == b"0100"
|
||||
|
@ -182,6 +186,7 @@ def test_mp_no_data() -> None:
|
|||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_mp_attribute(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
mpinfo = im._getmp()
|
||||
assert mpinfo is not None
|
||||
for frame_number, mpentry in enumerate(mpinfo[0xB002]):
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_equal
|
||||
|
||||
|
||||
def test_load_raw() -> None:
|
||||
with Image.open("Tests/images/hopper.pcd") as im:
|
||||
assert im.size == (768, 512)
|
||||
im.load() # should not segfault.
|
||||
|
||||
# Note that this image was created with a resized hopper
|
||||
|
@ -15,3 +22,18 @@ def test_load_raw() -> None:
|
|||
|
||||
# target = hopper().resize((768,512))
|
||||
# assert_image_similar(im, target, 10)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("orientation", (1, 3))
|
||||
def test_rotated(orientation: int) -> None:
|
||||
with open("Tests/images/hopper.pcd", "rb") as fp:
|
||||
data = bytearray(fp.read())
|
||||
data[2048 + 1538] = orientation
|
||||
f = BytesIO(data)
|
||||
with Image.open(f) as im:
|
||||
assert im.size == (512, 768)
|
||||
|
||||
with Image.open("Tests/images/hopper.pcd") as expected:
|
||||
assert_image_equal(
|
||||
im, expected.rotate(90 if orientation == 1 else 270, expand=True)
|
||||
)
|
||||
|
|
|
@ -229,7 +229,9 @@ class TestFilePng:
|
|||
assert_image(im, "RGBA", (162, 150))
|
||||
|
||||
# image has 124 unique alpha values
|
||||
assert len(im.getchannel("A").getcolors()) == 124
|
||||
colors = im.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert len(colors) == 124
|
||||
|
||||
def test_load_transparent_rgb(self) -> None:
|
||||
test_file = "Tests/images/rgb_trns.png"
|
||||
|
@ -241,7 +243,9 @@ class TestFilePng:
|
|||
assert_image(im, "RGBA", (64, 64))
|
||||
|
||||
# image has 876 transparent pixels
|
||||
assert im.getchannel("A").getcolors()[0][0] == 876
|
||||
colors = im.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == 876
|
||||
|
||||
def test_save_p_transparent_palette(self, tmp_path: Path) -> None:
|
||||
in_file = "Tests/images/pil123p.png"
|
||||
|
@ -262,7 +266,9 @@ class TestFilePng:
|
|||
assert_image(im, "RGBA", (162, 150))
|
||||
|
||||
# image has 124 unique alpha values
|
||||
assert len(im.getchannel("A").getcolors()) == 124
|
||||
colors = im.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert len(colors) == 124
|
||||
|
||||
def test_save_p_single_transparency(self, tmp_path: Path) -> None:
|
||||
in_file = "Tests/images/p_trns_single.png"
|
||||
|
@ -285,7 +291,9 @@ class TestFilePng:
|
|||
assert im.getpixel((31, 31)) == (0, 255, 52, 0)
|
||||
|
||||
# image has 876 transparent pixels
|
||||
assert im.getchannel("A").getcolors()[0][0] == 876
|
||||
colors = im.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == 876
|
||||
|
||||
def test_save_p_transparent_black(self, tmp_path: Path) -> None:
|
||||
# check if solid black image with full transparency
|
||||
|
@ -313,7 +321,9 @@ class TestFilePng:
|
|||
assert im.info["transparency"] == 255
|
||||
|
||||
im_rgba = im.convert("RGBA")
|
||||
assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
|
||||
colors = im_rgba.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == num_transparent
|
||||
|
||||
test_file = tmp_path / "temp.png"
|
||||
im.save(test_file)
|
||||
|
@ -324,7 +334,9 @@ class TestFilePng:
|
|||
assert_image_equal(im, test_im)
|
||||
|
||||
test_im_rgba = test_im.convert("RGBA")
|
||||
assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent
|
||||
colors = test_im_rgba.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == num_transparent
|
||||
|
||||
def test_save_rgb_single_transparency(self, tmp_path: Path) -> None:
|
||||
in_file = "Tests/images/caption_6_33_22.png"
|
||||
|
@ -671,6 +683,9 @@ class TestFilePng:
|
|||
im.save(out, bits=4, save_all=save_all)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
|
||||
assert reloaded.png is not None
|
||||
assert reloaded.png.im_palette is not None
|
||||
assert len(reloaded.png.im_palette[1]) == 48
|
||||
|
||||
def test_plte_length(self, tmp_path: Path) -> None:
|
||||
|
@ -681,6 +696,9 @@ class TestFilePng:
|
|||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
|
||||
assert reloaded.png is not None
|
||||
assert reloaded.png.im_palette is not None
|
||||
assert len(reloaded.png.im_palette[1]) == 3
|
||||
|
||||
def test_getxmp(self) -> None:
|
||||
|
@ -702,13 +720,17 @@ class TestFilePng:
|
|||
def test_exif(self) -> None:
|
||||
# With an EXIF chunk
|
||||
with Image.open("Tests/images/exif.png") as im:
|
||||
exif = im._getexif()
|
||||
assert exif[274] == 1
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
exif_data = im._getexif()
|
||||
assert exif_data is not None
|
||||
assert exif_data[274] == 1
|
||||
|
||||
# With an ImageMagick zTXt chunk
|
||||
with Image.open("Tests/images/exif_imagemagick.png") as im:
|
||||
exif = im._getexif()
|
||||
assert exif[274] == 1
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
exif_data = im._getexif()
|
||||
assert exif_data is not None
|
||||
assert exif_data[274] == 1
|
||||
|
||||
# Assert that info still can be extracted
|
||||
# when the image is no longer a PngImageFile instance
|
||||
|
@ -717,8 +739,10 @@ class TestFilePng:
|
|||
|
||||
# With a tEXt chunk
|
||||
with Image.open("Tests/images/exif_text.png") as im:
|
||||
exif = im._getexif()
|
||||
assert exif[274] == 1
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
exif_data = im._getexif()
|
||||
assert exif_data is not None
|
||||
assert exif_data[274] == 1
|
||||
|
||||
# With XMP tags
|
||||
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
||||
|
@ -740,8 +764,10 @@ class TestFilePng:
|
|||
im.save(test_file, exif=im.getexif())
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
exif = reloaded._getexif()
|
||||
assert exif[274] == 1
|
||||
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
|
||||
exif_data = reloaded._getexif()
|
||||
assert exif_data is not None
|
||||
assert exif_data[274] == 1
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
|
|
|
@ -92,6 +92,13 @@ def test_16bit_pgm() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff")
|
||||
|
||||
|
||||
def test_p4_save(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/hopper_1bit.pbm") as im:
|
||||
filename = tmp_path / "temp.pbm"
|
||||
im.save(filename)
|
||||
assert_image_equal_tofile(im, filename)
|
||||
|
||||
|
||||
def test_16bit_pgm_write(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/16_bit_binary.pgm") as im:
|
||||
filename = tmp_path / "temp.pgm"
|
||||
|
@ -134,6 +141,12 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
|
|||
assert_image_equal_tofile(im, filename)
|
||||
|
||||
|
||||
def test_save_unsupported_mode(tmp_path: Path) -> None:
|
||||
im = hopper("P")
|
||||
with pytest.raises(OSError, match="cannot write mode P as PPM"):
|
||||
im.save(tmp_path / "out.ppm")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
[
|
||||
|
|
|
@ -274,13 +274,17 @@ def test_save_l_transparency(tmp_path: Path) -> None:
|
|||
in_file = "Tests/images/la.tga"
|
||||
with Image.open(in_file) as im:
|
||||
assert im.mode == "LA"
|
||||
assert im.getchannel("A").getcolors()[0][0] == num_transparent
|
||||
colors = im.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == num_transparent
|
||||
|
||||
out = tmp_path / "temp.tga"
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as test_im:
|
||||
assert test_im.mode == "LA"
|
||||
assert test_im.getchannel("A").getcolors()[0][0] == num_transparent
|
||||
colors = test_im.getchannel("A").getcolors()
|
||||
assert colors is not None
|
||||
assert colors[0][0] == num_transparent
|
||||
|
||||
assert_image_equal(im, test_im)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import WalImageFile
|
||||
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
@ -13,12 +15,22 @@ def test_open() -> None:
|
|||
assert im.format_description == "Quake2 Texture"
|
||||
assert im.mode == "P"
|
||||
assert im.size == (128, 128)
|
||||
assert "next_name" not in im.info
|
||||
|
||||
assert isinstance(im, WalImageFile.WalImageFile)
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
|
||||
|
||||
|
||||
def test_next_name() -> None:
|
||||
with open(TEST_FILE, "rb") as fp:
|
||||
data = bytearray(fp.read())
|
||||
data[56:60] = b"Test"
|
||||
f = BytesIO(data)
|
||||
with WalImageFile.open(f) as im:
|
||||
assert im.info["next_name"] == b"Test"
|
||||
|
||||
|
||||
def test_load() -> None:
|
||||
with WalImageFile.open(TEST_FILE) as im:
|
||||
px = im.load()
|
||||
|
|
|
@ -4,13 +4,13 @@ from collections.abc import Generator
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
|
||||
from PIL import GifImagePlugin, Image, WebPImagePlugin
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
assert_image_similar,
|
||||
has_feature_version,
|
||||
is_big_endian,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
@ -53,11 +53,8 @@ def test_write_animation_L(tmp_path: Path) -> None:
|
|||
im.load()
|
||||
assert_image_similar(im, orig.convert("RGBA"), 32.9)
|
||||
|
||||
if is_big_endian():
|
||||
version = features.version_module("webp")
|
||||
assert version is not None
|
||||
if parse_version(version) < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
if is_big_endian() and not has_feature_version("webp", "1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
orig.seek(orig.n_frames - 1)
|
||||
im.seek(im.n_frames - 1)
|
||||
orig.load()
|
||||
|
@ -81,11 +78,8 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
|||
assert_image_equal(im, frame1.convert("RGBA"))
|
||||
|
||||
# Compare second frame to original
|
||||
if is_big_endian():
|
||||
version = features.version_module("webp")
|
||||
assert version is not None
|
||||
if parse_version(version) < parse_version("1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
if is_big_endian() and not has_feature_version("webp", "1.2.2"):
|
||||
pytest.skip("Fails with libwebp earlier than 1.2.2")
|
||||
im.seek(1)
|
||||
im.load()
|
||||
assert_image_equal(im, frame2.convert("RGBA"))
|
||||
|
|
|
@ -22,11 +22,13 @@ except ImportError:
|
|||
def test_read_exif_metadata() -> None:
|
||||
file_path = "Tests/images/flower.webp"
|
||||
with Image.open(file_path) as image:
|
||||
assert isinstance(image, WebPImagePlugin.WebPImageFile)
|
||||
assert image.format == "WEBP"
|
||||
exif_data = image.info.get("exif", None)
|
||||
assert exif_data
|
||||
|
||||
exif = image._getexif()
|
||||
assert exif is not None
|
||||
|
||||
# Camera make
|
||||
assert exif[271] == "Canon"
|
||||
|
|
|
@ -9,7 +9,8 @@ from .helper import skip_unless_feature
|
|||
|
||||
class TestFontCrash:
|
||||
def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None:
|
||||
# from fuzzers.fuzz_font
|
||||
# Copy of the code from fuzz_font() in Tests/oss-fuzz/fuzzers.py
|
||||
# that triggered a problem when fuzzing
|
||||
font.getbbox("ABC")
|
||||
font.getmask("test text")
|
||||
with Image.new(mode="RGBA", size=(200, 200)) as im:
|
||||
|
|
|
@ -19,6 +19,7 @@ from PIL import (
|
|||
ImageDraw,
|
||||
ImageFile,
|
||||
ImagePalette,
|
||||
ImageShow,
|
||||
UnidentifiedImageError,
|
||||
features,
|
||||
)
|
||||
|
@ -283,33 +284,6 @@ class TestImage:
|
|||
assert item is not None
|
||||
assert item != num
|
||||
|
||||
def test_expand_x(self) -> None:
|
||||
# Arrange
|
||||
im = hopper()
|
||||
orig_size = im.size
|
||||
xmargin = 5
|
||||
|
||||
# Act
|
||||
im = im._expand(xmargin)
|
||||
|
||||
# Assert
|
||||
assert im.size[0] == orig_size[0] + 2 * xmargin
|
||||
assert im.size[1] == orig_size[1] + 2 * xmargin
|
||||
|
||||
def test_expand_xy(self) -> None:
|
||||
# Arrange
|
||||
im = hopper()
|
||||
orig_size = im.size
|
||||
xmargin = 5
|
||||
ymargin = 3
|
||||
|
||||
# Act
|
||||
im = im._expand(xmargin, ymargin)
|
||||
|
||||
# Assert
|
||||
assert im.size[0] == orig_size[0] + 2 * xmargin
|
||||
assert im.size[1] == orig_size[1] + 2 * ymargin
|
||||
|
||||
def test_getbands(self) -> None:
|
||||
# Assert
|
||||
assert hopper("RGB").getbands() == ("R", "G", "B")
|
||||
|
@ -388,6 +362,37 @@ class TestImage:
|
|||
assert img_colors is not None
|
||||
assert sorted(img_colors) == expected_colors
|
||||
|
||||
def test_alpha_composite_la(self) -> None:
|
||||
# Arrange
|
||||
expected_colors = sorted(
|
||||
[
|
||||
(3300, (255, 255)),
|
||||
(1156, (170, 192)),
|
||||
(1122, (128, 255)),
|
||||
(1089, (0, 0)),
|
||||
(1122, (255, 128)),
|
||||
(1122, (0, 128)),
|
||||
(1089, (0, 255)),
|
||||
]
|
||||
)
|
||||
|
||||
dst = Image.new("LA", size=(100, 100), color=(0, 255))
|
||||
draw = ImageDraw.Draw(dst)
|
||||
draw.rectangle((0, 33, 100, 66), fill=(0, 128))
|
||||
draw.rectangle((0, 67, 100, 100), fill=(0, 0))
|
||||
src = Image.new("LA", size=(100, 100), color=(255, 255))
|
||||
draw = ImageDraw.Draw(src)
|
||||
draw.rectangle((33, 0, 66, 100), fill=(255, 128))
|
||||
draw.rectangle((67, 0, 100, 100), fill=(255, 0))
|
||||
|
||||
# Act
|
||||
img = Image.alpha_composite(dst, src)
|
||||
|
||||
# Assert
|
||||
img_colors = img.getcolors()
|
||||
assert img_colors is not None
|
||||
assert sorted(img_colors) == expected_colors
|
||||
|
||||
def test_alpha_inplace(self) -> None:
|
||||
src = Image.new("RGBA", (128, 128), "blue")
|
||||
|
||||
|
@ -922,6 +927,17 @@ class TestImage:
|
|||
reloaded_exif.load(exif.tobytes())
|
||||
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
|
||||
|
||||
def test_delete_ifd_tag(self) -> None:
|
||||
with Image.open("Tests/images/flower.jpg") as im:
|
||||
exif = im.getexif()
|
||||
exif.get_ifd(0x8769)
|
||||
assert 0x8769 in exif
|
||||
del exif[0x8769]
|
||||
|
||||
reloaded_exif = Image.Exif()
|
||||
reloaded_exif.load(exif.tobytes())
|
||||
assert 0x8769 not in reloaded_exif
|
||||
|
||||
def test_exif_load_from_fp(self) -> None:
|
||||
with Image.open("Tests/images/flower.jpg") as im:
|
||||
data = im.info["exif"]
|
||||
|
@ -1005,6 +1021,13 @@ class TestImage:
|
|||
with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
|
||||
assert im.get_child_images() == []
|
||||
|
||||
def test_show(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(ImageShow, "_viewers", [])
|
||||
|
||||
im = Image.new("RGB", (1, 1))
|
||||
with pytest.warns(DeprecationWarning, match="Image._show"):
|
||||
Image._show(im)
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||
im = Image.new("RGB", size)
|
||||
|
@ -1076,6 +1099,12 @@ class TestImage:
|
|||
assert im.palette is not None
|
||||
assert im.palette.colors[(27, 35, 6, 214)] == 24
|
||||
|
||||
def test_merge_pa(self) -> None:
|
||||
p = hopper("P")
|
||||
a = Image.new("L", p.size)
|
||||
pa = Image.merge("PA", (p, a))
|
||||
assert p.getpalette() == pa.getpalette()
|
||||
|
||||
def test_constants(self) -> None:
|
||||
for enum in (
|
||||
Image.Transpose,
|
||||
|
|
|
@ -101,9 +101,8 @@ def test_fromarray_strides_without_tobytes() -> None:
|
|||
self.__array_interface__ = arr_params
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
|
||||
with pytest.warns(DeprecationWarning, match="'mode' parameter"):
|
||||
Image.fromarray(wrapped, "L")
|
||||
wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1), "typestr": "|u1"})
|
||||
Image.fromarray(wrapped, "L")
|
||||
|
||||
|
||||
def test_fromarray_palette() -> None:
|
||||
|
@ -112,9 +111,16 @@ def test_fromarray_palette() -> None:
|
|||
a = numpy.array(i)
|
||||
|
||||
# Act
|
||||
with pytest.warns(DeprecationWarning, match="'mode' parameter"):
|
||||
out = Image.fromarray(a, "P")
|
||||
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
|
||||
|
||||
|
||||
def test_deprecation() -> None:
|
||||
a = numpy.array(im.convert("L"))
|
||||
with pytest.warns(
|
||||
DeprecationWarning, match="'mode' parameter for changing data types"
|
||||
):
|
||||
Image.fromarray(a, "1")
|
||||
|
|
|
@ -97,6 +97,13 @@ def test_opaque() -> None:
|
|||
assert_image_equal(alpha, solid)
|
||||
|
||||
|
||||
def test_rgba() -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
assert im.mode == "RGBA"
|
||||
|
||||
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
|
||||
|
||||
|
||||
def test_rgba_p() -> None:
|
||||
im = hopper("RGBA")
|
||||
im.putalpha(hopper("L"))
|
||||
|
@ -107,11 +114,19 @@ def test_rgba_p() -> None:
|
|||
assert_image_similar(im, comparable, 20)
|
||||
|
||||
|
||||
def test_rgba() -> None:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
assert im.mode == "RGBA"
|
||||
def test_rgba_pa() -> None:
|
||||
im = hopper("RGBA").convert("PA").convert("RGB")
|
||||
expected = hopper("RGB")
|
||||
|
||||
assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
|
||||
assert_image_similar(im, expected, 9.3)
|
||||
|
||||
|
||||
def test_pa() -> None:
|
||||
im = hopper().convert("PA")
|
||||
|
||||
palette = im.palette
|
||||
assert palette is not None
|
||||
assert palette.colors != {}
|
||||
|
||||
|
||||
def test_trns_p(tmp_path: Path) -> None:
|
||||
|
|
|
@ -124,6 +124,21 @@ class TestImagingPaste:
|
|||
im = im.crop((12, 23, im2.width + 12, im2.height + 23))
|
||||
assert_image_equal(im, im2)
|
||||
|
||||
@pytest.mark.parametrize("y", [10, -10])
|
||||
@pytest.mark.parametrize("mode", ["L", "RGB"])
|
||||
@pytest.mark.parametrize("mask_mode", ["", "1", "L", "LA", "RGBa"])
|
||||
def test_image_self(self, y: int, mode: str, mask_mode: str) -> None:
|
||||
im = getattr(self, "gradient_" + mode)
|
||||
mask = Image.new(mask_mode, im.size, 0xFFFFFFFF) if mask_mode else None
|
||||
|
||||
im_self = im.copy()
|
||||
im_self.paste(im_self, (0, y), mask)
|
||||
|
||||
im_copy = im.copy()
|
||||
im_copy.paste(im_copy.copy(), (0, y), mask)
|
||||
|
||||
assert_image_equal(im_self, im_copy)
|
||||
|
||||
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
|
||||
def test_image_mask_1(self, mode: str) -> None:
|
||||
im = Image.new(mode, (200, 200), "white")
|
||||
|
|
|
@ -62,6 +62,7 @@ def test_putpalette_with_alpha_values() -> None:
|
|||
expected = im.convert("RGBA")
|
||||
|
||||
palette = im.getpalette()
|
||||
assert palette is not None
|
||||
transparency = im.info.pop("transparency")
|
||||
|
||||
palette_with_alpha_values = []
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, features
|
||||
from PIL import Image
|
||||
|
||||
from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature
|
||||
from .helper import (
|
||||
assert_image_similar,
|
||||
has_feature_version,
|
||||
hopper,
|
||||
is_ppc64le,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
|
@ -23,11 +28,8 @@ def test_sanity() -> None:
|
|||
@skip_unless_feature("libimagequant")
|
||||
def test_libimagequant_quantize() -> None:
|
||||
image = hopper()
|
||||
if is_ppc64le():
|
||||
version = features.version_feature("libimagequant")
|
||||
assert version is not None
|
||||
if parse_version(version) < parse_version("4"):
|
||||
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
|
||||
if is_ppc64le() and not has_feature_version("libimagequant", "4"):
|
||||
pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le")
|
||||
converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT)
|
||||
assert converted.mode == "P"
|
||||
assert_image_similar(converted.convert("RGB"), image, 15)
|
||||
|
@ -116,6 +118,15 @@ def test_quantize_kmeans(method: Image.Quantize) -> None:
|
|||
im.quantize(kmeans=-1, method=method)
|
||||
|
||||
|
||||
@skip_unless_feature("libimagequant")
|
||||
def test_resize() -> None:
|
||||
im = hopper().resize((100, 100))
|
||||
converted = im.quantize(100, Image.Quantize.LIBIMAGEQUANT)
|
||||
colors = converted.getcolors()
|
||||
assert colors is not None
|
||||
assert len(colors) == 100
|
||||
|
||||
|
||||
def test_colors() -> None:
|
||||
im = hopper()
|
||||
colors = 2
|
||||
|
|
|
@ -1494,7 +1494,9 @@ def test_default_font_size() -> None:
|
|||
|
||||
def draw_text() -> None:
|
||||
draw.text((0, 0), text, font_size=16)
|
||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
||||
assert_image_similar_tofile(
|
||||
im, "Tests/images/imagedraw_default_font_size.png", 1
|
||||
)
|
||||
|
||||
check(draw_text)
|
||||
|
||||
|
@ -1513,7 +1515,9 @@ def test_default_font_size() -> None:
|
|||
|
||||
def draw_multiline_text() -> None:
|
||||
draw.multiline_text((0, 0), text, font_size=16)
|
||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
|
||||
assert_image_similar_tofile(
|
||||
im, "Tests/images/imagedraw_default_font_size.png", 1
|
||||
)
|
||||
|
||||
check(draw_multiline_text)
|
||||
|
||||
|
|
|
@ -164,6 +164,11 @@ class TestImageFile:
|
|||
with pytest.raises(OSError):
|
||||
p.close()
|
||||
|
||||
def test_negative_offset(self) -> None:
|
||||
with Image.open("Tests/images/raw_negative_stride.bin") as im:
|
||||
with pytest.raises(ValueError, match="Tile offset cannot be negative"):
|
||||
im.load()
|
||||
|
||||
def test_no_format(self) -> None:
|
||||
buf = BytesIO(b"\x00" * 255)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ from .helper import (
|
|||
assert_image_equal,
|
||||
assert_image_equal_tofile,
|
||||
assert_image_similar_tofile,
|
||||
has_feature_version,
|
||||
is_win32,
|
||||
skip_unless_feature,
|
||||
skip_unless_feature_version,
|
||||
|
@ -492,6 +493,11 @@ def test_stroke_mask() -> None:
|
|||
assert mask.getpixel((42, 5)) == 255
|
||||
|
||||
|
||||
def test_load_invalid_file() -> None:
|
||||
with pytest.raises(SyntaxError, match="Not a PILfont file"):
|
||||
ImageFont.load("Tests/images/1_trns.png")
|
||||
|
||||
|
||||
def test_load_when_image_not_found() -> None:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
pass
|
||||
|
@ -549,7 +555,7 @@ def test_default_font() -> None:
|
|||
draw.text((10, 60), txt, font=larger_default_font)
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
|
||||
assert_image_similar_tofile(im, "Tests/images/default_font_freetype.png", 0.13)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("", "1", "RGBA"))
|
||||
|
@ -1055,7 +1061,10 @@ def test_colr(layout_engine: ImageFont.Layout) -> None:
|
|||
|
||||
d.text((15, 5), "Bungee", font=font, embedded_color=True)
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 21)
|
||||
if has_feature_version("freetype2", "2.14.0"):
|
||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee.png", 6.1)
|
||||
else:
|
||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee_older.png", 21)
|
||||
|
||||
|
||||
@skip_unless_feature_version("freetype2", "2.10.0")
|
||||
|
@ -1071,7 +1080,7 @@ def test_colr_mask(layout_engine: ImageFont.Layout) -> None:
|
|||
|
||||
d.text((15, 5), "Bungee", "black", font=font)
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
|
||||
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 14.1)
|
||||
|
||||
|
||||
def test_woff2(layout_engine: ImageFont.Layout) -> None:
|
||||
|
|
|
@ -7,6 +7,7 @@ from PIL import Image, ImageDraw, ImageFont
|
|||
from .helper import (
|
||||
assert_image_equal_tofile,
|
||||
assert_image_similar_tofile,
|
||||
has_feature_version,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
||||
|
@ -104,11 +105,9 @@ def test_text_direction_ttb() -> None:
|
|||
|
||||
im = Image.new(mode="RGB", size=(100, 300))
|
||||
draw = ImageDraw.Draw(im)
|
||||
try:
|
||||
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
|
||||
except ValueError as ex:
|
||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
if not has_feature_version("raqm", "0.7"):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb")
|
||||
|
||||
target = "Tests/images/test_direction_ttb.png"
|
||||
assert_image_similar_tofile(im, target, 2.8)
|
||||
|
@ -119,19 +118,17 @@ def test_text_direction_ttb_stroke() -> None:
|
|||
|
||||
im = Image.new(mode="RGB", size=(100, 300))
|
||||
draw = ImageDraw.Draw(im)
|
||||
try:
|
||||
draw.text(
|
||||
(27, 27),
|
||||
"あい",
|
||||
font=ttf,
|
||||
fill=500,
|
||||
direction="ttb",
|
||||
stroke_width=2,
|
||||
stroke_fill="#0f0",
|
||||
)
|
||||
except ValueError as ex:
|
||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
if not has_feature_version("raqm", "0.7"):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
draw.text(
|
||||
(27, 27),
|
||||
"あい",
|
||||
font=ttf,
|
||||
fill=500,
|
||||
direction="ttb",
|
||||
stroke_width=2,
|
||||
stroke_fill="#0f0",
|
||||
)
|
||||
|
||||
target = "Tests/images/test_direction_ttb_stroke.png"
|
||||
assert_image_similar_tofile(im, target, 19.4)
|
||||
|
@ -186,7 +183,7 @@ def test_x_max_and_y_offset() -> None:
|
|||
draw.text((0, 0), "لح", font=ttf, fill=500)
|
||||
|
||||
target = "Tests/images/test_x_max_and_y_offset.png"
|
||||
assert_image_similar_tofile(im, target, 0.5)
|
||||
assert_image_similar_tofile(im, target, 3.8)
|
||||
|
||||
|
||||
def test_language() -> None:
|
||||
|
@ -219,14 +216,9 @@ def test_getlength(
|
|||
im = Image.new(mode, (1, 1), 0)
|
||||
d = ImageDraw.Draw(im)
|
||||
|
||||
try:
|
||||
assert d.textlength(text, ttf, direction) == expected
|
||||
except ValueError as ex:
|
||||
if (
|
||||
direction == "ttb"
|
||||
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
|
||||
):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
assert d.textlength(text, ttf, direction) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("L", "1"))
|
||||
|
@ -242,17 +234,12 @@ def test_getlength_combine(mode: str, direction: str, text: str) -> None:
|
|||
|
||||
ttf = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
||||
|
||||
try:
|
||||
target = ttf.getlength("ii", mode, direction)
|
||||
actual = ttf.getlength(text, mode, direction)
|
||||
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
target = ttf.getlength("ii", mode, direction)
|
||||
actual = ttf.getlength(text, mode, direction)
|
||||
|
||||
assert actual == target
|
||||
except ValueError as ex:
|
||||
if (
|
||||
direction == "ttb"
|
||||
and str(ex) == "libraqm 0.7 or greater required for 'ttb' direction"
|
||||
):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
assert actual == target
|
||||
|
||||
|
||||
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
|
||||
|
@ -265,11 +252,9 @@ def test_anchor_ttb(anchor: str) -> None:
|
|||
d = ImageDraw.Draw(im)
|
||||
d.line(((0, 200), (200, 200)), "gray")
|
||||
d.line(((100, 0), (100, 400)), "gray")
|
||||
try:
|
||||
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
|
||||
except ValueError as ex:
|
||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
if not has_feature_version("raqm", "0.7"):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
d.text((100, 200), text, fill="black", anchor=anchor, direction="ttb", font=f)
|
||||
|
||||
assert_image_similar_tofile(im, path, 1) # fails at 5
|
||||
|
||||
|
@ -310,10 +295,12 @@ combine_tests = (
|
|||
|
||||
# this tests various combining characters for anchor alignment and clipping
|
||||
@pytest.mark.parametrize(
|
||||
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
|
||||
"name, text, anchor, direction, epsilon",
|
||||
combine_tests,
|
||||
ids=[r[0] for r in combine_tests],
|
||||
)
|
||||
def test_combine(
|
||||
name: str, text: str, dir: str | None, anchor: str | None, epsilon: float
|
||||
name: str, text: str, direction: str | None, anchor: str | None, epsilon: float
|
||||
) -> None:
|
||||
path = f"Tests/images/test_combine_{name}.png"
|
||||
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
||||
|
@ -322,11 +309,9 @@ def test_combine(
|
|||
d = ImageDraw.Draw(im)
|
||||
d.line(((0, 200), (400, 200)), "gray")
|
||||
d.line(((200, 0), (200, 400)), "gray")
|
||||
try:
|
||||
d.text((200, 200), text, fill="black", anchor=anchor, direction=dir, font=f)
|
||||
except ValueError as ex:
|
||||
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
if direction == "ttb" and not has_feature_version("raqm", "0.7"):
|
||||
pytest.skip("libraqm 0.7 or greater not available")
|
||||
d.text((200, 200), text, fill="black", anchor=anchor, direction=direction, font=f)
|
||||
|
||||
assert_image_similar_tofile(im, path, epsilon)
|
||||
|
||||
|
|
|
@ -30,6 +30,14 @@ def test_default_font(font: ImageFont.ImageFont) -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/default_font.png")
|
||||
|
||||
|
||||
def test_invalid_mode() -> None:
|
||||
font = ImageFont.ImageFont()
|
||||
fp = BytesIO()
|
||||
with Image.open("Tests/images/hopper.png") as im:
|
||||
with pytest.raises(TypeError, match="invalid font image mode"):
|
||||
font._load_pilfont_data(fp, im)
|
||||
|
||||
|
||||
def test_without_freetype() -> None:
|
||||
original_core = ImageFont.core
|
||||
if features.check_module("freetype2"):
|
||||
|
|
|
@ -7,7 +7,7 @@ import pytest
|
|||
|
||||
from PIL import Image, ImageMorph, _imagingmorph
|
||||
|
||||
from .helper import assert_image_equal_tofile, hopper
|
||||
from .helper import assert_image_equal_tofile, hopper, timeout_unless_slower_valgrind
|
||||
|
||||
|
||||
def string_to_img(image_string: str) -> Image.Image:
|
||||
|
@ -266,16 +266,18 @@ def test_unknown_pattern() -> None:
|
|||
ImageMorph.LutBuilder(op_name="unknown")
|
||||
|
||||
|
||||
def test_pattern_syntax_error() -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"pattern", ("a pattern with a syntax error", "4:(" + "X" * 30000)
|
||||
)
|
||||
@timeout_unless_slower_valgrind(1)
|
||||
def test_pattern_syntax_error(pattern: str) -> None:
|
||||
# Arrange
|
||||
lb = ImageMorph.LutBuilder(op_name="corner")
|
||||
new_patterns = ["a pattern with a syntax error"]
|
||||
new_patterns = [pattern]
|
||||
lb.add_patterns(new_patterns)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(
|
||||
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
|
||||
):
|
||||
with pytest.raises(Exception, match='Syntax error in pattern "'):
|
||||
lb.build_lut()
|
||||
|
||||
|
||||
|
|
|
@ -186,6 +186,21 @@ def test_palette(mode: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_rgba_palette() -> None:
|
||||
im = Image.new("P", (1, 1))
|
||||
|
||||
red = (255, 0, 0, 255)
|
||||
translucent_black = (0, 0, 0, 127)
|
||||
im.putpalette(red + translucent_black, "RGBA")
|
||||
|
||||
expanded_im = ImageOps.expand(im, 1, 1)
|
||||
|
||||
palette = expanded_im.palette
|
||||
assert palette is not None
|
||||
assert palette.mode == "RGBA"
|
||||
assert expanded_im.convert("RGBA").getpixel((0, 0)) == translucent_black
|
||||
|
||||
|
||||
def test_pil163() -> None:
|
||||
# Division by zero in equalize if < 255 pixels in image (@PIL163)
|
||||
|
||||
|
|
|
@ -76,9 +76,14 @@ def test_consecutive() -> None:
|
|||
def test_palette_mmap() -> None:
|
||||
# Using mmap in ImageFile can require to reload the palette.
|
||||
with Image.open("Tests/images/multipage-mmap.tiff") as im:
|
||||
color1 = im.getpalette()[:3]
|
||||
palette = im.getpalette()
|
||||
assert palette is not None
|
||||
color1 = palette[:3]
|
||||
im.seek(0)
|
||||
color2 = im.getpalette()[:3]
|
||||
|
||||
palette = im.getpalette()
|
||||
assert palette is not None
|
||||
color2 = palette[:3]
|
||||
assert color1 == color2
|
||||
|
||||
|
||||
|
|
|
@ -59,15 +59,12 @@ def test_show(mode: str) -> None:
|
|||
assert ImageShow.show(im)
|
||||
|
||||
|
||||
def test_show_without_viewers() -> None:
|
||||
viewers = ImageShow._viewers
|
||||
ImageShow._viewers = []
|
||||
def test_show_without_viewers(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(ImageShow, "_viewers", [])
|
||||
|
||||
with hopper() as im:
|
||||
assert not ImageShow.show(im)
|
||||
|
||||
ImageShow._viewers = viewers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"viewer",
|
||||
|
|
|
@ -57,3 +57,13 @@ def test_constant() -> None:
|
|||
assert st.rms[0] == 128
|
||||
assert st.var[0] == 0
|
||||
assert st.stddev[0] == 0
|
||||
|
||||
|
||||
def test_zero_count() -> None:
|
||||
im = Image.new("L", (0, 0))
|
||||
|
||||
st = ImageStat.Stat(im)
|
||||
|
||||
assert st.mean == [0]
|
||||
assert st.rms == [0]
|
||||
assert st.var == [0]
|
||||
|
|
302
Tests/test_nanoarrow.py
Normal file
|
@ -0,0 +1,302 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import (
|
||||
assert_deep_equal,
|
||||
assert_image_equal,
|
||||
hopper,
|
||||
is_big_endian,
|
||||
)
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
import nanoarrow # type: ignore [import-not-found]
|
||||
else:
|
||||
nanoarrow = pytest.importorskip("nanoarrow", reason="Nanoarrow not installed")
|
||||
|
||||
TEST_IMAGE_SIZE = (10, 10)
|
||||
|
||||
|
||||
def _test_img_equals_pyarray(
|
||||
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
|
||||
) -> None:
|
||||
assert img.height * img.width * elts_per_pixel == len(arr)
|
||||
px = img.load()
|
||||
assert px is not None
|
||||
if elts_per_pixel > 1 and mask is None:
|
||||
# have to do element-wise comparison when we're comparing
|
||||
# flattened r,g,b,a to a pixel.
|
||||
mask = list(range(elts_per_pixel))
|
||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||
if mask:
|
||||
pixel = px[x, y]
|
||||
assert isinstance(pixel, tuple)
|
||||
for ix, elt in enumerate(mask):
|
||||
if elts_per_pixel == 1:
|
||||
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
|
||||
else:
|
||||
assert (
|
||||
pixel[ix]
|
||||
== arr[(y * img.width + x) * elts_per_pixel + elt].as_py()
|
||||
)
|
||||
else:
|
||||
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
|
||||
|
||||
|
||||
def _test_img_equals_int32_pyarray(
|
||||
img: Image.Image, arr: Any, mask: list[int] | None, elts_per_pixel: int = 1
|
||||
) -> None:
|
||||
assert img.height * img.width * elts_per_pixel == len(arr)
|
||||
px = img.load()
|
||||
assert px is not None
|
||||
if mask is None:
|
||||
# have to do element-wise comparison when we're comparing
|
||||
# flattened rgba in an uint32 to a pixel.
|
||||
mask = list(range(elts_per_pixel))
|
||||
for x in range(0, img.size[0], int(img.size[0] / 10)):
|
||||
for y in range(0, img.size[1], int(img.size[1] / 10)):
|
||||
pixel = px[x, y]
|
||||
assert isinstance(pixel, tuple)
|
||||
arr_pixel_int = arr[y * img.width + x].as_py()
|
||||
arr_pixel_tuple = (
|
||||
arr_pixel_int % 256,
|
||||
(arr_pixel_int // 256) % 256,
|
||||
(arr_pixel_int // 256**2) % 256,
|
||||
(arr_pixel_int // 256**3),
|
||||
)
|
||||
if is_big_endian():
|
||||
arr_pixel_tuple = arr_pixel_tuple[::-1]
|
||||
|
||||
for ix, elt in enumerate(mask):
|
||||
assert pixel[ix] == arr_pixel_tuple[elt]
|
||||
|
||||
|
||||
fl_uint8_4_type = nanoarrow.fixed_size_list(
|
||||
value_type=nanoarrow.uint8(nullable=False), list_size=4, nullable=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, dtype, mask",
|
||||
(
|
||||
("L", nanoarrow.uint8(nullable=False), None),
|
||||
("I", nanoarrow.int32(nullable=False), None),
|
||||
("F", nanoarrow.float32(nullable=False), None),
|
||||
("LA", fl_uint8_4_type, [0, 3]),
|
||||
("RGB", fl_uint8_4_type, [0, 1, 2]),
|
||||
("RGBA", fl_uint8_4_type, None),
|
||||
("RGBX", fl_uint8_4_type, None),
|
||||
("CMYK", fl_uint8_4_type, None),
|
||||
("YCbCr", fl_uint8_4_type, [0, 1, 2]),
|
||||
("HSV", fl_uint8_4_type, [0, 1, 2]),
|
||||
),
|
||||
)
|
||||
def test_to_array(mode: str, dtype: nanoarrow, mask: list[int] | None) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
# Resize to non-square
|
||||
img = img.crop((3, 0, 124, 127))
|
||||
assert img.size == (121, 127)
|
||||
|
||||
arr = nanoarrow.Array(img)
|
||||
_test_img_equals_pyarray(img, arr, mask)
|
||||
assert arr.schema.type == dtype.type
|
||||
assert arr.schema.nullable == dtype.nullable
|
||||
|
||||
reloaded = Image.fromarrow(arr, mode, img.size)
|
||||
assert_image_equal(img, reloaded)
|
||||
|
||||
|
||||
def test_lifetime() -> None:
|
||||
# valgrind shouldn't error out here.
|
||||
# arrays should be accessible after the image is deleted.
|
||||
|
||||
img = hopper("L")
|
||||
|
||||
arr_1 = nanoarrow.Array(img)
|
||||
arr_2 = nanoarrow.Array(img)
|
||||
|
||||
del img
|
||||
|
||||
assert sum(arr_1.iter_py()) > 0
|
||||
del arr_1
|
||||
|
||||
assert sum(arr_2.iter_py()) > 0
|
||||
del arr_2
|
||||
|
||||
|
||||
def test_lifetime2() -> None:
|
||||
# valgrind shouldn't error out here.
|
||||
# img should remain after the arrays are collected.
|
||||
|
||||
img = hopper("L")
|
||||
|
||||
arr_1 = nanoarrow.Array(img)
|
||||
arr_2 = nanoarrow.Array(img)
|
||||
|
||||
assert sum(arr_1.iter_py()) > 0
|
||||
del arr_1
|
||||
|
||||
assert sum(arr_2.iter_py()) > 0
|
||||
del arr_2
|
||||
|
||||
img2 = img.copy()
|
||||
px = img2.load()
|
||||
assert px # make mypy happy
|
||||
assert isinstance(px[0, 0], int)
|
||||
|
||||
|
||||
class DataShape(NamedTuple):
|
||||
dtype: nanoarrow
|
||||
# Strictly speaking, elt should be a pixel or pixel component, so
|
||||
# list[uint8][4], float, int, uint32, uint8, etc. But more
|
||||
# correctly, it should be exactly the dtype from the line above.
|
||||
elt: Any
|
||||
elts_per_pixel: int
|
||||
|
||||
|
||||
UINT_ARR = DataShape(
|
||||
dtype=fl_uint8_4_type,
|
||||
elt=[1, 2, 3, 4], # array of 4 uint8 per pixel
|
||||
elts_per_pixel=1, # only one array per pixel
|
||||
)
|
||||
|
||||
UINT = DataShape(
|
||||
dtype=nanoarrow.uint8(),
|
||||
elt=3, # one uint8,
|
||||
elts_per_pixel=4, # but repeated 4x per pixel
|
||||
)
|
||||
|
||||
UINT32 = DataShape(
|
||||
dtype=nanoarrow.uint32(),
|
||||
elt=0xABCDEF45, # one packed int, doesn't fit in a int32 > 0x80000000
|
||||
elts_per_pixel=1, # one per pixel
|
||||
)
|
||||
|
||||
INT32 = DataShape(
|
||||
dtype=nanoarrow.uint32(),
|
||||
elt=0x12CDEF45, # one packed int
|
||||
elts_per_pixel=1, # one per pixel
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, data_tp, mask",
|
||||
(
|
||||
("L", DataShape(nanoarrow.uint8(), 3, 1), None),
|
||||
("I", DataShape(nanoarrow.int32(), 1 << 24, 1), None),
|
||||
("F", DataShape(nanoarrow.float32(), 3.14159, 1), None),
|
||||
("LA", UINT_ARR, [0, 3]),
|
||||
("LA", UINT, [0, 3]),
|
||||
("RGB", UINT_ARR, [0, 1, 2]),
|
||||
("RGBA", UINT_ARR, None),
|
||||
("CMYK", UINT_ARR, None),
|
||||
("YCbCr", UINT_ARR, [0, 1, 2]),
|
||||
("HSV", UINT_ARR, [0, 1, 2]),
|
||||
("RGB", UINT, [0, 1, 2]),
|
||||
("RGBA", UINT, None),
|
||||
("CMYK", UINT, None),
|
||||
("YCbCr", UINT, [0, 1, 2]),
|
||||
("HSV", UINT, [0, 1, 2]),
|
||||
),
|
||||
)
|
||||
def test_fromarray(mode: str, data_tp: DataShape, mask: list[int] | None) -> None:
|
||||
(dtype, elt, elts_per_pixel) = data_tp
|
||||
|
||||
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||
if dtype == fl_uint8_4_type:
|
||||
tmp_arr = nanoarrow.Array(
|
||||
elt * (ct_pixels * elts_per_pixel), schema=nanoarrow.uint8()
|
||||
)
|
||||
c_array = nanoarrow.c_array_from_buffers(
|
||||
dtype, ct_pixels, buffers=[], children=[tmp_arr]
|
||||
)
|
||||
arr = nanoarrow.Array(c_array)
|
||||
else:
|
||||
arr = nanoarrow.Array(
|
||||
nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)
|
||||
)
|
||||
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
|
||||
|
||||
_test_img_equals_pyarray(img, arr, mask, elts_per_pixel)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, mask",
|
||||
(
|
||||
("LA", [0, 3]),
|
||||
("RGB", [0, 1, 2]),
|
||||
("RGBA", None),
|
||||
("CMYK", None),
|
||||
("YCbCr", [0, 1, 2]),
|
||||
("HSV", [0, 1, 2]),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("data_tp", (UINT32, INT32))
|
||||
def test_from_int32array(mode: str, mask: list[int] | None, data_tp: DataShape) -> None:
|
||||
(dtype, elt, elts_per_pixel) = data_tp
|
||||
|
||||
ct_pixels = TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1]
|
||||
arr = nanoarrow.Array(
|
||||
nanoarrow.c_array([elt] * (ct_pixels * elts_per_pixel), schema=dtype)
|
||||
)
|
||||
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
|
||||
|
||||
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, metadata",
|
||||
(
|
||||
("LA", ["L", "X", "X", "A"]),
|
||||
("RGB", ["R", "G", "B", "X"]),
|
||||
("RGBX", ["R", "G", "B", "X"]),
|
||||
("RGBA", ["R", "G", "B", "A"]),
|
||||
("CMYK", ["C", "M", "Y", "K"]),
|
||||
("YCbCr", ["Y", "Cb", "Cr", "X"]),
|
||||
("HSV", ["H", "S", "V", "X"]),
|
||||
),
|
||||
)
|
||||
def test_image_nested_metadata(mode: str, metadata: list[str]) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
arr = nanoarrow.Array(img)
|
||||
|
||||
assert arr.schema.value_type.metadata
|
||||
assert arr.schema.value_type.metadata[b"image"]
|
||||
|
||||
parsed_metadata = json.loads(
|
||||
arr.schema.value_type.metadata[b"image"].decode("utf8")
|
||||
)
|
||||
|
||||
assert "bands" in parsed_metadata
|
||||
assert parsed_metadata["bands"] == metadata
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, metadata",
|
||||
(
|
||||
("L", ["L"]),
|
||||
("I", ["I"]),
|
||||
("F", ["F"]),
|
||||
),
|
||||
)
|
||||
def test_image_flat_metadata(mode: str, metadata: list[str]) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
arr = nanoarrow.Array(img)
|
||||
|
||||
assert arr.schema.metadata
|
||||
assert arr.schema.metadata[b"image"]
|
||||
|
||||
parsed_metadata = json.loads(arr.schema.metadata[b"image"].decode("utf8"))
|
||||
|
||||
assert "bands" in parsed_metadata
|
||||
assert parsed_metadata["bands"] == metadata
|
|
@ -28,15 +28,13 @@ def test_numpy_to_image() -> None:
|
|||
a = numpy.array(data, dtype=dtype)
|
||||
a.shape = TEST_IMAGE_SIZE
|
||||
i = Image.fromarray(a)
|
||||
if list(i.getdata()) != data:
|
||||
print("data mismatch for", dtype)
|
||||
assert list(i.getdata()) == data
|
||||
else:
|
||||
data = list(range(100))
|
||||
a = numpy.array([[x] * bands for x in data], dtype=dtype)
|
||||
a.shape = TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1], bands
|
||||
i = Image.fromarray(a)
|
||||
if list(i.getchannel(0).getdata()) != list(range(100)):
|
||||
print("data mismatch for", dtype)
|
||||
assert list(i.getchannel(0).getdata()) == list(range(100))
|
||||
return i
|
||||
|
||||
# Check supported 1-bit integer formats
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
|
@ -244,3 +245,29 @@ def test_from_int32array(mode: str, data_tp: DataShape, mask: list[int] | None)
|
|||
img = Image.fromarrow(arr, mode, TEST_IMAGE_SIZE)
|
||||
|
||||
_test_img_equals_int32_pyarray(img, arr, mask, elts_per_pixel)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode, metadata",
|
||||
(
|
||||
("LA", ["L", "X", "X", "A"]),
|
||||
("RGB", ["R", "G", "B", "X"]),
|
||||
("RGBX", ["R", "G", "B", "X"]),
|
||||
("RGBA", ["R", "G", "B", "A"]),
|
||||
("CMYK", ["C", "M", "Y", "K"]),
|
||||
("YCbCr", ["Y", "Cb", "Cr", "X"]),
|
||||
("HSV", ["H", "S", "V", "X"]),
|
||||
),
|
||||
)
|
||||
def test_image_metadata(mode: str, metadata: list[str]) -> None:
|
||||
img = hopper(mode)
|
||||
|
||||
arr = pyarrow.array(img) # type: ignore[call-overload]
|
||||
|
||||
assert arr.type.field(0).metadata
|
||||
assert arr.type.field(0).metadata[b"image"]
|
||||
|
||||
parsed_metadata = json.loads(arr.type.field(0).metadata[b"image"].decode("utf8"))
|
||||
|
||||
assert "bands" in parsed_metadata
|
||||
assert parsed_metadata["bands"] == metadata
|
||||
|
|
|
@ -9,7 +9,7 @@ from PIL import __version__
|
|||
pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed")
|
||||
|
||||
|
||||
def map_metadata_keys(metadata):
|
||||
def map_metadata_keys(md):
|
||||
# Convert installed wheel metadata into canonical Core Metadata 2.4 format.
|
||||
# This was a utility method in pyroma 4.3.3; it was removed in 5.0.
|
||||
# This implementation is constructed from the relevant logic from
|
||||
|
@ -17,8 +17,8 @@ def map_metadata_keys(metadata):
|
|||
# upstream to Pyroma as https://github.com/regebro/pyroma/pull/116,
|
||||
# so it may be possible to simplify this test in future.
|
||||
data = {}
|
||||
for key in set(metadata.keys()):
|
||||
value = metadata.get_all(key)
|
||||
for key in set(md.keys()):
|
||||
value = md.get_all(key)
|
||||
key = pyroma.projectdata.normalize(key)
|
||||
|
||||
if len(value) == 1:
|
||||
|
|
1
checks/32bit_segfault_check.py
Executable file → Normal file
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
|
8
checks/check_imaging_leaks.py
Executable file → Normal file
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
|
@ -8,12 +8,12 @@ import pytest
|
|||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import is_win32
|
||||
|
||||
min_iterations = 100
|
||||
max_iterations = 10000
|
||||
|
||||
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
sys.platform.startswith("win32"), reason="requires Unix or macOS"
|
||||
)
|
||||
|
||||
|
||||
def _get_mem_usage() -> float:
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .helper import is_win32, skip_unless_feature
|
||||
from PIL import Image, features
|
||||
|
||||
# Limits for testing the leak
|
||||
mem_limit = 1024 * 1048576
|
||||
|
@ -15,8 +14,10 @@ iterations = int((mem_limit / stack_size) * 2)
|
|||
test_file = "Tests/images/rgb_trns_ycbc.jp2"
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"),
|
||||
skip_unless_feature("jpg_2000"),
|
||||
pytest.mark.skipif(
|
||||
sys.platform.startswith("win32"), reason="requires Unix or macOS"
|
||||
),
|
||||
pytest.mark.skipif(not features.check("jpg_2000"), reason="jpg_2000 not available"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from .helper import hopper, is_win32
|
||||
from PIL import Image
|
||||
|
||||
iterations = 5000
|
||||
|
||||
|
@ -18,7 +19,9 @@ valgrind --tool=massif python test-installed.py -s -v checks/check_jpeg_leaks.py
|
|||
"""
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
sys.platform.startswith("win32"), reason="requires Unix or macOS"
|
||||
)
|
||||
|
||||
"""
|
||||
pre patch:
|
||||
|
@ -112,10 +115,10 @@ standard_chrominance_qtable = (
|
|||
),
|
||||
)
|
||||
def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
|
||||
im = hopper("RGB")
|
||||
for _ in range(iterations):
|
||||
test_output = BytesIO()
|
||||
im.save(test_output, "JPEG", qtables=qtables)
|
||||
with Image.open("Tests/images/hopper.ppm") as im:
|
||||
for _ in range(iterations):
|
||||
test_output = BytesIO()
|
||||
im.save(test_output, "JPEG", qtables=qtables)
|
||||
|
||||
|
||||
def test_exif_leak() -> None:
|
||||
|
@ -173,12 +176,12 @@ def test_exif_leak() -> None:
|
|||
0 +----------------------------------------------------------------------->Gi
|
||||
0 11.33
|
||||
"""
|
||||
im = hopper("RGB")
|
||||
exif = b"12345678" * 4096
|
||||
|
||||
for _ in range(iterations):
|
||||
test_output = BytesIO()
|
||||
im.save(test_output, "JPEG", exif=exif)
|
||||
with Image.open("Tests/images/hopper.ppm") as im:
|
||||
for _ in range(iterations):
|
||||
test_output = BytesIO()
|
||||
im.save(test_output, "JPEG", exif=exif)
|
||||
|
||||
|
||||
def test_base_save() -> None:
|
||||
|
@ -207,8 +210,7 @@ def test_base_save() -> None:
|
|||
| :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@:::
|
||||
0 +----------------------------------------------------------------------->Gi
|
||||
0 7.882"""
|
||||
im = hopper("RGB")
|
||||
|
||||
for _ in range(iterations):
|
||||
test_output = BytesIO()
|
||||
im.save(test_output, "JPEG")
|
||||
with Image.open("Tests/images/hopper.ppm") as im:
|
||||
for _ in range(iterations):
|
||||
test_output = BytesIO()
|
||||
im.save(test_output, "JPEG")
|
||||
|
|
|
@ -4,7 +4,6 @@ import platform
|
|||
import sys
|
||||
|
||||
from PIL import features
|
||||
from Tests.helper import is_pypy
|
||||
|
||||
|
||||
def test_wheel_modules() -> None:
|
||||
|
@ -48,8 +47,6 @@ def test_wheel_features() -> None:
|
|||
|
||||
if sys.platform == "win32":
|
||||
expected_features.remove("xcb")
|
||||
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
|
||||
expected_features.remove("zlib_ng")
|
||||
elif sys.platform == "ios":
|
||||
# Can't distribute raqm due to licensing, and there's no system version;
|
||||
# fribidi and harfbuzz won't be available if raqm isn't available.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# install libimagequant
|
||||
|
||||
archive_name=libimagequant
|
||||
archive_version=4.3.4
|
||||
archive_version=4.4.0
|
||||
|
||||
archive=$archive_name-$archive_version
|
||||
|
||||
|
|
|
@ -59,6 +59,6 @@ cmake \
|
|||
"${LIBAVIF_CMAKE_FLAGS[@]}" \
|
||||
.
|
||||
|
||||
sudo make install
|
||||
make install
|
||||
|
||||
popd
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install openjpeg
|
||||
|
||||
archive=openjpeg-2.5.3
|
||||
archive=openjpeg-2.5.4
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
# install raqm
|
||||
|
||||
|
||||
archive=libraqm-0.10.2
|
||||
archive=libraqm-0.10.3
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
pushd $archive
|
||||
|
||||
meson build --prefix=/usr && sudo ninja -C build install
|
||||
meson build --prefix=/usr && ninja -C build install
|
||||
|
||||
popd
|
||||
|
|
|
@ -35,8 +35,12 @@ Image.fromarray mode parameter
|
|||
|
||||
.. deprecated:: 11.3.0
|
||||
|
||||
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The
|
||||
mode can be automatically determined from the object's shape and type instead.
|
||||
Using the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was deprecated in
|
||||
Pillow 11.3.0. In Pillow 12.0.0, this was partially reverted, and it is now only
|
||||
deprecated when changing data types. Since pixel values do not contain information
|
||||
about palettes or color spaces, the parameter can still be used to place grayscale L
|
||||
mode data within a P mode image, or read RGB data as YCbCr for example. If omitted, the
|
||||
mode will be automatically determined from the object's shape and type.
|
||||
|
||||
Saving I mode images as PNG
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -61,6 +65,14 @@ ImageCms.ImageCmsProfile.product_name and .product_info
|
|||
``.product_info`` attributes have been deprecated, and will be removed in
|
||||
Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
|
||||
|
||||
Image._show
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 12.0.0
|
||||
|
||||
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
|
||||
Use :py:meth:`~PIL.ImageShow.show` instead.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ Here is a list of PyPI projects that offer additional plugins:
|
|||
* :pypi:`heif-image-plugin`: Simple HEIF/HEIC images plugin, based on the pyheif library.
|
||||
* :pypi:`jxlpy`: Introduces reading and writing support for JPEG XL.
|
||||
* :pypi:`pillow-heif`: Python bindings to libheif for working with HEIF images.
|
||||
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implemetation. Python bindings implemented using pybind11.
|
||||
* :pypi:`pillow-jpls`: Plugin for the JPEG-LS codec, based on the Charls JPEG-LS implementation. Python bindings implemented using pybind11.
|
||||
* :pypi:`pillow-jxl-plugin`: Plugin for JPEG-XL, using Rust for bindings.
|
||||
* :pypi:`pillow-mbm`: Adds support for KSP's proprietary MBM texture format.
|
||||
* :pypi:`pillow-svg`: Implements basic SVG read support. Supports basic paths, shapes, and text.
|
||||
|
|
|
@ -44,7 +44,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libtiff** provides compressed TIFF functionality
|
||||
|
||||
* Pillow has been tested with libtiff versions **4.0-4.7.0**
|
||||
* Pillow has been tested with libtiff versions **4.0-4.7.1**
|
||||
|
||||
* **libfreetype** provides type related services
|
||||
|
||||
|
@ -58,13 +58,13 @@ Many of Pillow's features require external libraries:
|
|||
* **openjpeg** provides JPEG 2000 functionality.
|
||||
|
||||
* Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
|
||||
**2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**.
|
||||
**2.4.0**, **2.5.0**, **2.5.2**, **2.5.3** and **2.5.4**.
|
||||
* Pillow does **not** support the earlier **1.5** series which ships
|
||||
with Debian Jessie.
|
||||
|
||||
* **libimagequant** provides improved color quantization
|
||||
|
||||
* Pillow has been tested with libimagequant **2.6-4.3.4**
|
||||
* Pillow has been tested with libimagequant **2.6-4.4.0**
|
||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||
the Pillow license, therefore we will not be distributing binaries
|
||||
with libimagequant support enabled.
|
||||
|
|
|
@ -31,15 +31,17 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 13 Trixie | 3.13 | x86, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 41 | 3.13 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 42 | 3.13 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Gentoo | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| macOS 13 Ventura | 3.10 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| macOS 14 Sonoma | 3.11, 3.12, 3.13, 3.14 | arm64 |
|
||||
| macOS 15 Sequoia | 3.10 | x86-64 |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.11, 3.12, 3.13, 3.14, | arm64 |
|
||||
| | PyPy3 | |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
|
||||
|
@ -73,6 +75,8 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+============================+==================+==============+
|
||||
| macOS 26 Tahoe | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.3.0 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.8 | 10.4.0 | |
|
||||
|
|
|
@ -57,6 +57,43 @@ Color names
|
|||
|
||||
See :ref:`color-names` for the color names supported by Pillow.
|
||||
|
||||
Alpha channel
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
By default, when drawing onto an existing image, the image's pixel values are simply
|
||||
replaced by the new color::
|
||||
|
||||
im = Image.new("RGBA", (1, 1), (255, 0, 0))
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle((0, 0, 1, 1), (0, 255, 0, 127))
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 127)
|
||||
|
||||
# Alpha channel values have no effect when drawing with RGB mode
|
||||
im = Image.new("RGB", (1, 1), (255, 0, 0))
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle((0, 0, 1, 1), (0, 255, 0, 127))
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0)
|
||||
|
||||
If you would like to combine translucent color with an RGB image, then initialize the
|
||||
ImageDraw instance with the RGBA mode::
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
im = Image.new("RGB", (1, 1), (255, 0, 0))
|
||||
d = ImageDraw.Draw(im, "RGBA")
|
||||
d.rectangle((0, 0, 1, 1), (0, 255, 0, 127))
|
||||
assert im.getpixel((0, 0)) == (128, 127, 0)
|
||||
|
||||
If you would like to combine translucent color with an RGBA image underneath, you will
|
||||
need to combine multiple images::
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
im = Image.new("RGBA", (1, 1), (255, 0, 0, 255))
|
||||
im2 = Image.new("RGBA", (1, 1))
|
||||
d = ImageDraw.Draw(im2)
|
||||
d.rectangle((0, 0, 1, 1), (0, 255, 0, 127))
|
||||
im.paste(im2.convert("RGB"), mask=im2)
|
||||
assert im.getpixel((0, 0)) == (128, 127, 0, 255)
|
||||
|
||||
Fonts
|
||||
^^^^^
|
||||
|
||||
|
|
|
@ -74,5 +74,6 @@ Constants
|
|||
---------
|
||||
|
||||
.. autodata:: PIL.ImageFile.LOAD_TRUNCATED_IMAGES
|
||||
.. autodata:: PIL.ImageFile.MAXBLOCK
|
||||
.. autodata:: PIL.ImageFile.ERRORS
|
||||
:annotation:
|
||||
|
|
|
@ -20,7 +20,9 @@ or the clipboard to a PIL image memory.
|
|||
used as a fallback if they are installed. To disable this behaviour, pass
|
||||
``xdisplay=""`` instead.
|
||||
|
||||
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux)
|
||||
.. versionadded:: 1.1.3 Windows support
|
||||
.. versionadded:: 3.0.0 macOS support
|
||||
.. versionadded:: 7.1.0 Linux support
|
||||
|
||||
:param bbox: What region to copy. Default is the entire screen.
|
||||
On macOS, this is not increased to 2x for Retina screens, so the full
|
||||
|
@ -53,7 +55,9 @@ or the clipboard to a PIL image memory.
|
|||
|
||||
On Linux, ``wl-paste`` or ``xclip`` is required.
|
||||
|
||||
.. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux)
|
||||
.. versionadded:: 1.1.4 Windows support
|
||||
.. versionadded:: 3.3.0 macOS support
|
||||
.. versionadded:: 9.4.0 Linux support
|
||||
|
||||
:return: On Windows, an image, a list of filenames,
|
||||
or None if the clipboard does not contain image data or filenames.
|
||||
|
|
|
@ -29,6 +29,13 @@ Image.fromarray mode parameter
|
|||
The ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` has been deprecated. The
|
||||
mode can be automatically determined from the object's shape and type instead.
|
||||
|
||||
.. note::
|
||||
|
||||
Since pixel values do not contain information about palettes or color spaces, part
|
||||
of this functionality was restored in Pillow 12.0.0. The parameter can be used to
|
||||
place grayscale L mode data within a P mode image, or read RGB data as YCbCr for
|
||||
example.
|
||||
|
||||
Saving I mode images as PNG
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -1,19 +1,6 @@
|
|||
12.0.0
|
||||
------
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards incompatible changes
|
||||
==============================
|
||||
|
||||
|
@ -116,6 +103,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`).
|
|||
Deprecations
|
||||
============
|
||||
|
||||
Image._show
|
||||
^^^^^^^^^^^
|
||||
|
||||
``Image._show`` has been deprecated, and will be removed in Pillow 13 (2026-10-15).
|
||||
Use :py:meth:`~PIL.ImageShow.show` instead.
|
||||
|
||||
ImageCms.ImageCmsProfile.product_name and .product_info
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -126,18 +119,10 @@ Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0.
|
|||
API changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
Image.alpha_composite: LA images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API additions
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
:py:meth:`~PIL.Image.alpha_composite` can now use LA images as well as RGBA.
|
||||
|
||||
Other changes
|
||||
=============
|
||||
|
@ -150,3 +135,19 @@ others prepare for 3.14, and to ensure Pillow could be used immediately at the r
|
|||
of 3.14.0 final (2025-10-07, :pep:`745`).
|
||||
|
||||
Pillow 12.0.0 now officially supports Python 3.14.
|
||||
|
||||
Image.fromarray mode parameter
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In Pillow 11.3.0, the ``mode`` parameter in :py:meth:`~PIL.Image.fromarray()` was
|
||||
deprecated. Part of this functionality has been restored in Pillow 12.0.0. Since pixel
|
||||
values do not contain information about palettes or color spaces, the parameter can be
|
||||
used to place grayscale L mode data within a P mode image, or read RGB data as YCbCr
|
||||
for example.
|
||||
|
||||
ImageMorph operations must have length 1
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Valid ImageMorph operations are 4, N, 1 and M. By limiting the length to 1 character
|
||||
within Pillow, long execution times can be avoided if a user provided long pattern
|
||||
strings. Reported by `Jang Choi <https://github.com/uko3211>`__.
|
||||
|
|
|
@ -57,6 +57,9 @@ optional-dependencies.mic = [
|
|||
"olefile",
|
||||
]
|
||||
optional-dependencies.test-arrow = [
|
||||
"arro3-compute",
|
||||
"arro3-core",
|
||||
"nanoarrow",
|
||||
"pyarrow",
|
||||
]
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from . import BmpImagePlugin, Image, ImageFile
|
||||
from . import BmpImagePlugin, Image
|
||||
from ._binary import i16le as i16
|
||||
from ._binary import i32le as i32
|
||||
|
||||
|
@ -38,6 +38,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
|
|||
format_description = "Windows Cursor"
|
||||
|
||||
def _open(self) -> None:
|
||||
assert self.fp is not None
|
||||
offset = self.fp.tell()
|
||||
|
||||
# check magic
|
||||
|
@ -63,8 +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] = ImageFile._Tile(d, (0, 0) + self.size, o, a)
|
||||
self.tile = [self.tile[0]._replace(extents=(0, 0) + self.size)]
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -354,6 +354,9 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
read_comment(s)
|
||||
elif bytes_mv[:9] == b"%%Trailer":
|
||||
trailer_reached = True
|
||||
elif bytes_mv[:14] == b"%%BeginBinary:":
|
||||
bytecount = int(byte_arr[14:bytes_read])
|
||||
self.fp.seek(bytecount, os.SEEK_CUR)
|
||||
bytes_read = 0
|
||||
|
||||
# A "BoundingBox" is always required,
|
||||
|
|
|
@ -30,7 +30,7 @@ from ._util import DeferredError
|
|||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return (
|
||||
len(prefix) >= 6
|
||||
len(prefix) >= 16
|
||||
and i16(prefix, 4) in [0xAF11, 0xAF12]
|
||||
and i16(prefix, 14) in [0, 3] # flags
|
||||
)
|
||||
|
@ -48,8 +48,14 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
|
||||
def _open(self) -> None:
|
||||
# HEAD
|
||||
assert self.fp is not None
|
||||
s = self.fp.read(128)
|
||||
if not (_accept(s) and s[20:22] == b"\x00\x00"):
|
||||
if not (
|
||||
_accept(s)
|
||||
and s[20:22] == b"\x00" * 2
|
||||
and s[42:80] == b"\x00" * 38
|
||||
and s[88:] == b"\x00" * 40
|
||||
):
|
||||
msg = "not an FLI/FLC file"
|
||||
raise SyntaxError(msg)
|
||||
|
||||
|
@ -77,8 +83,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
|
||||
if i16(s, 4) == 0xF100:
|
||||
# prefix chunk; ignore it
|
||||
self.__offset = self.__offset + i32(s)
|
||||
self.fp.seek(self.__offset)
|
||||
self.fp.seek(self.__offset + i32(s))
|
||||
s = self.fp.read(16)
|
||||
|
||||
if i16(s, 4) == 0xF1FA:
|
||||
|
@ -111,6 +116,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
# load palette
|
||||
|
||||
i = 0
|
||||
assert self.fp is not None
|
||||
for e in range(i16(self.fp.read(2))):
|
||||
s = self.fp.read(2)
|
||||
i = i + s[0]
|
||||
|
|
|
@ -54,7 +54,7 @@ class GbrImageFile(ImageFile.ImageFile):
|
|||
width = i32(self.fp.read(4))
|
||||
height = i32(self.fp.read(4))
|
||||
color_depth = i32(self.fp.read(4))
|
||||
if width <= 0 or height <= 0:
|
||||
if width == 0 or height == 0:
|
||||
msg = "not a GIMP brush"
|
||||
raise SyntaxError(msg)
|
||||
if color_depth not in (1, 4):
|
||||
|
@ -71,7 +71,7 @@ class GbrImageFile(ImageFile.ImageFile):
|
|||
raise SyntaxError(msg)
|
||||
self.info["spacing"] = i32(self.fp.read(4))
|
||||
|
||||
comment = self.fp.read(comment_length)[:-1]
|
||||
self.info["comment"] = self.fp.read(comment_length)[:-1]
|
||||
|
||||
if color_depth == 1:
|
||||
self._mode = "L"
|
||||
|
@ -80,8 +80,6 @@ class GbrImageFile(ImageFile.ImageFile):
|
|||
|
||||
self._size = width, height
|
||||
|
||||
self.info["comment"] = comment
|
||||
|
||||
# Image might not be small
|
||||
Image._decompression_bomb_check(self.size)
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None:
|
|||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix.startswith(b"GRIB") and prefix[7] == 1
|
||||
return len(prefix) >= 8 and prefix.startswith(b"GRIB") and prefix[7] == 1
|
||||
|
||||
|
||||
class GribStubImageFile(ImageFile.StubImageFile):
|
||||
|
|
115
src/PIL/Image.py
|
@ -103,7 +103,6 @@ try:
|
|||
raise ImportError(msg)
|
||||
|
||||
except ImportError as v:
|
||||
core = DeferredError.new(ImportError("The _imaging C module is not installed."))
|
||||
# Explanations for ways that we know we might have an import error
|
||||
if str(v).startswith("Module use of python"):
|
||||
# The _imaging C module is present, but not compiled for
|
||||
|
@ -1010,8 +1009,14 @@ class Image:
|
|||
new_im.info["transparency"] = transparency
|
||||
return new_im
|
||||
|
||||
if mode == "P" and self.mode == "RGBA":
|
||||
return self.quantize(colors)
|
||||
if self.mode == "RGBA":
|
||||
if mode == "P":
|
||||
return self.quantize(colors)
|
||||
elif mode == "PA":
|
||||
r, g, b, a = self.split()
|
||||
rgb = merge("RGB", (r, g, b))
|
||||
p = rgb.quantize(colors)
|
||||
return merge("PA", (p, a))
|
||||
|
||||
trns = None
|
||||
delete_trns = False
|
||||
|
@ -1143,7 +1148,7 @@ class Image:
|
|||
raise ValueError(msg) from e
|
||||
|
||||
new_im = self._new(im)
|
||||
if mode == "P" and palette != Palette.ADAPTIVE:
|
||||
if mode in ("P", "PA") and palette != Palette.ADAPTIVE:
|
||||
from . import ImagePalette
|
||||
|
||||
new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
|
||||
|
@ -1337,12 +1342,6 @@ class Image:
|
|||
"""
|
||||
pass
|
||||
|
||||
def _expand(self, xmargin: int, ymargin: int | None = None) -> Image:
|
||||
if ymargin is None:
|
||||
ymargin = xmargin
|
||||
self.load()
|
||||
return self._new(self.im.expand(xmargin, ymargin))
|
||||
|
||||
def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
|
||||
"""
|
||||
Filters this image using the given filter. For a list of
|
||||
|
@ -2071,9 +2070,7 @@ class Image:
|
|||
:param value: The pixel value.
|
||||
"""
|
||||
|
||||
if self.readonly:
|
||||
self._copy()
|
||||
self.load()
|
||||
self._ensure_mutable()
|
||||
|
||||
if (
|
||||
self.mode in ("P", "PA")
|
||||
|
@ -2633,7 +2630,9 @@ class Image:
|
|||
:param title: Optional title to use for the image window, where possible.
|
||||
"""
|
||||
|
||||
_show(self, title=title)
|
||||
from . import ImageShow
|
||||
|
||||
ImageShow.show(self, title)
|
||||
|
||||
def split(self) -> tuple[Image, ...]:
|
||||
"""
|
||||
|
@ -3258,19 +3257,10 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
|
|||
transferred. This means that P and PA mode images will lose their palette.
|
||||
|
||||
:param obj: Object with array interface
|
||||
:param mode: Optional mode to use when reading ``obj``. Will be determined from
|
||||
type if ``None``. Deprecated.
|
||||
|
||||
This will not be used to convert the data after reading, but will be used to
|
||||
change how the data is read::
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
a = np.full((1, 1), 300)
|
||||
im = Image.fromarray(a, mode="L")
|
||||
im.getpixel((0, 0)) # 44
|
||||
im = Image.fromarray(a, mode="RGB")
|
||||
im.getpixel((0, 0)) # (44, 1, 0)
|
||||
:param mode: Optional mode to use when reading ``obj``. Since pixel values do not
|
||||
contain information about palettes or color spaces, this can be used to place
|
||||
grayscale L mode data within a P mode image, or read RGB data as YCbCr for
|
||||
example.
|
||||
|
||||
See: :ref:`concept-modes` for general information about modes.
|
||||
:returns: An image object.
|
||||
|
@ -3281,21 +3271,28 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
|
|||
shape = arr["shape"]
|
||||
ndim = len(shape)
|
||||
strides = arr.get("strides", None)
|
||||
if mode is None:
|
||||
try:
|
||||
typekey = (1, 1) + shape[2:], arr["typestr"]
|
||||
except KeyError as e:
|
||||
try:
|
||||
typekey = (1, 1) + shape[2:], arr["typestr"]
|
||||
except KeyError as e:
|
||||
if mode is not None:
|
||||
typekey = None
|
||||
color_modes: list[str] = []
|
||||
else:
|
||||
msg = "Cannot handle this data type"
|
||||
raise TypeError(msg) from e
|
||||
if typekey is not None:
|
||||
try:
|
||||
mode, rawmode = _fromarray_typemap[typekey]
|
||||
typemode, rawmode, color_modes = _fromarray_typemap[typekey]
|
||||
except KeyError as e:
|
||||
typekey_shape, typestr = typekey
|
||||
msg = f"Cannot handle this data type: {typekey_shape}, {typestr}"
|
||||
raise TypeError(msg) from e
|
||||
else:
|
||||
deprecate("'mode' parameter", 13)
|
||||
if mode is not None:
|
||||
if mode != typemode and mode not in color_modes:
|
||||
deprecate("'mode' parameter for changing data types", 13)
|
||||
rawmode = mode
|
||||
else:
|
||||
mode = typemode
|
||||
if mode in ["1", "L", "I", "P", "F"]:
|
||||
ndmax = 2
|
||||
elif mode == "RGB":
|
||||
|
@ -3392,29 +3389,29 @@ def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile:
|
|||
|
||||
|
||||
_fromarray_typemap = {
|
||||
# (shape, typestr) => mode, rawmode
|
||||
# (shape, typestr) => mode, rawmode, color modes
|
||||
# first two members of shape are set to one
|
||||
((1, 1), "|b1"): ("1", "1;8"),
|
||||
((1, 1), "|u1"): ("L", "L"),
|
||||
((1, 1), "|i1"): ("I", "I;8"),
|
||||
((1, 1), "<u2"): ("I", "I;16"),
|
||||
((1, 1), ">u2"): ("I", "I;16B"),
|
||||
((1, 1), "<i2"): ("I", "I;16S"),
|
||||
((1, 1), ">i2"): ("I", "I;16BS"),
|
||||
((1, 1), "<u4"): ("I", "I;32"),
|
||||
((1, 1), ">u4"): ("I", "I;32B"),
|
||||
((1, 1), "<i4"): ("I", "I;32S"),
|
||||
((1, 1), ">i4"): ("I", "I;32BS"),
|
||||
((1, 1), "<f4"): ("F", "F;32F"),
|
||||
((1, 1), ">f4"): ("F", "F;32BF"),
|
||||
((1, 1), "<f8"): ("F", "F;64F"),
|
||||
((1, 1), ">f8"): ("F", "F;64BF"),
|
||||
((1, 1, 2), "|u1"): ("LA", "LA"),
|
||||
((1, 1, 3), "|u1"): ("RGB", "RGB"),
|
||||
((1, 1, 4), "|u1"): ("RGBA", "RGBA"),
|
||||
((1, 1), "|b1"): ("1", "1;8", []),
|
||||
((1, 1), "|u1"): ("L", "L", ["P"]),
|
||||
((1, 1), "|i1"): ("I", "I;8", []),
|
||||
((1, 1), "<u2"): ("I", "I;16", []),
|
||||
((1, 1), ">u2"): ("I", "I;16B", []),
|
||||
((1, 1), "<i2"): ("I", "I;16S", []),
|
||||
((1, 1), ">i2"): ("I", "I;16BS", []),
|
||||
((1, 1), "<u4"): ("I", "I;32", []),
|
||||
((1, 1), ">u4"): ("I", "I;32B", []),
|
||||
((1, 1), "<i4"): ("I", "I;32S", []),
|
||||
((1, 1), ">i4"): ("I", "I;32BS", []),
|
||||
((1, 1), "<f4"): ("F", "F;32F", []),
|
||||
((1, 1), ">f4"): ("F", "F;32BF", []),
|
||||
((1, 1), "<f8"): ("F", "F;64F", []),
|
||||
((1, 1), ">f8"): ("F", "F;64BF", []),
|
||||
((1, 1, 2), "|u1"): ("LA", "LA", ["La", "PA"]),
|
||||
((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]),
|
||||
((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa", "RGBX", "CMYK"]),
|
||||
# shortcuts:
|
||||
((1, 1), f"{_ENDIAN}i4"): ("I", "I"),
|
||||
((1, 1), f"{_ENDIAN}f4"): ("F", "F"),
|
||||
((1, 1), f"{_ENDIAN}i4"): ("I", "I", []),
|
||||
((1, 1), f"{_ENDIAN}f4"): ("F", "F", []),
|
||||
}
|
||||
|
||||
|
||||
|
@ -3571,9 +3568,8 @@ def alpha_composite(im1: Image, im2: Image) -> Image:
|
|||
"""
|
||||
Alpha composite im2 over im1.
|
||||
|
||||
:param im1: The first image. Must have mode RGBA.
|
||||
:param im2: The second image. Must have mode RGBA, and the same size as
|
||||
the first image.
|
||||
:param im1: The first image. Must have mode RGBA or LA.
|
||||
:param im2: The second image. Must have the same mode and size as the first image.
|
||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||
"""
|
||||
|
||||
|
@ -3799,6 +3795,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
|
|||
def _show(image: Image, **options: Any) -> None:
|
||||
from . import ImageShow
|
||||
|
||||
deprecate("Image._show", 13, "ImageShow.show")
|
||||
ImageShow.show(image, **options)
|
||||
|
||||
|
||||
|
@ -4220,6 +4217,8 @@ class Exif(_ExifBase):
|
|||
del self._info[tag]
|
||||
else:
|
||||
del self._data[tag]
|
||||
if tag in self._ifds:
|
||||
del self._ifds[tag]
|
||||
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
keys = set(self._data)
|
||||
|
|
|
@ -74,9 +74,7 @@ class ImageDraw:
|
|||
must be the same as the image mode. If omitted, the mode
|
||||
defaults to the mode of the image.
|
||||
"""
|
||||
im.load()
|
||||
if im.readonly:
|
||||
im._copy() # make it writeable
|
||||
im._ensure_mutable()
|
||||
blend = 0
|
||||
if mode is None:
|
||||
mode = im.mode
|
||||
|
|
|
@ -46,6 +46,18 @@ if TYPE_CHECKING:
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAXBLOCK = 65536
|
||||
"""
|
||||
By default, Pillow processes image data in blocks. This helps to prevent excessive use
|
||||
of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``.
|
||||
|
||||
When reading an image, this is the number of bytes to read at once.
|
||||
|
||||
When writing an image, this is the number of bytes to write at once.
|
||||
If the image width times 4 is greater, then that will be used instead.
|
||||
Plugins may also set a greater number.
|
||||
|
||||
User code may set this to another number.
|
||||
"""
|
||||
|
||||
SAFEBLOCK = 1024 * 1024
|
||||
|
||||
|
@ -301,6 +313,9 @@ class ImageFile(Image.Image):
|
|||
and args[0] == self.mode
|
||||
and args[0] in Image._MAPMODES
|
||||
):
|
||||
if offset < 0:
|
||||
msg = "Tile offset cannot be negative"
|
||||
raise ValueError(msg)
|
||||
try:
|
||||
# use mmap, if possible
|
||||
import mmap
|
||||
|
|
|
@ -125,11 +125,16 @@ class ImageFont:
|
|||
image.close()
|
||||
|
||||
def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
|
||||
# check image
|
||||
if image.mode not in ("1", "L"):
|
||||
msg = "invalid font image mode"
|
||||
raise TypeError(msg)
|
||||
|
||||
# read PILfont header
|
||||
if file.readline() != b"PILfont\n":
|
||||
if file.read(8) != b"PILfont\n":
|
||||
msg = "Not a PILfont file"
|
||||
raise SyntaxError(msg)
|
||||
file.readline().split(b";")
|
||||
file.readline()
|
||||
self.info = [] # FIXME: should be a dictionary
|
||||
while True:
|
||||
s = file.readline()
|
||||
|
@ -140,11 +145,6 @@ class ImageFont:
|
|||
# read PILfont metrics
|
||||
data = file.read(256 * 20)
|
||||
|
||||
# check image
|
||||
if image.mode not in ("1", "L"):
|
||||
msg = "invalid font image mode"
|
||||
raise TypeError(msg)
|
||||
|
||||
image.load()
|
||||
|
||||
self.font = Image.core.font(image.im, data)
|
||||
|
@ -671,11 +671,7 @@ class FreeTypeFont:
|
|||
:returns: A list of the named styles in a variation font.
|
||||
:exception OSError: If the font is not a variation font.
|
||||
"""
|
||||
try:
|
||||
names = self.font.getvarnames()
|
||||
except AttributeError as e:
|
||||
msg = "FreeType 2.9.1 or greater is required"
|
||||
raise NotImplementedError(msg) from e
|
||||
names = self.font.getvarnames()
|
||||
return [name.replace(b"\x00", b"") for name in names]
|
||||
|
||||
def set_variation_by_name(self, name: str | bytes) -> None:
|
||||
|
@ -702,11 +698,7 @@ class FreeTypeFont:
|
|||
:returns: A list of the axes in a variation font.
|
||||
:exception OSError: If the font is not a variation font.
|
||||
"""
|
||||
try:
|
||||
axes = self.font.getvaraxes()
|
||||
except AttributeError as e:
|
||||
msg = "FreeType 2.9.1 or greater is required"
|
||||
raise NotImplementedError(msg) from e
|
||||
axes = self.font.getvaraxes()
|
||||
for axis in axes:
|
||||
if axis["name"]:
|
||||
axis["name"] = axis["name"].replace(b"\x00", b"")
|
||||
|
@ -717,11 +709,7 @@ class FreeTypeFont:
|
|||
:param axes: A list of values for each axis.
|
||||
:exception OSError: If the font is not a variation font.
|
||||
"""
|
||||
try:
|
||||
self.font.setvaraxes(axes)
|
||||
except AttributeError as e:
|
||||
msg = "FreeType 2.9.1 or greater is required"
|
||||
raise NotImplementedError(msg) from e
|
||||
self.font.setvaraxes(axes)
|
||||
|
||||
|
||||
class TransposedFont:
|
||||
|
|
|
@ -150,7 +150,7 @@ class LutBuilder:
|
|||
|
||||
# Parse and create symmetries of the patterns strings
|
||||
for p in self.patterns:
|
||||
m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
|
||||
m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
|
||||
if not m:
|
||||
msg = 'Syntax error in pattern "' + p + '"'
|
||||
raise Exception(msg)
|
||||
|
|
|
@ -499,14 +499,15 @@ def expand(
|
|||
height = top + image.size[1] + bottom
|
||||
color = _color(fill, image.mode)
|
||||
if image.palette:
|
||||
palette = ImagePalette.ImagePalette(palette=image.getpalette())
|
||||
mode = image.palette.mode
|
||||
palette = ImagePalette.ImagePalette(mode, image.getpalette(mode))
|
||||
if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
|
||||
color = palette.getcolor(color)
|
||||
else:
|
||||
palette = None
|
||||
out = Image.new(image.mode, (width, height), color)
|
||||
if palette:
|
||||
out.putpalette(palette.palette)
|
||||
out.putpalette(palette.palette, mode)
|
||||
out.paste(image, (left, top))
|
||||
return out
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@ class Stat:
|
|||
@cached_property
|
||||
def mean(self) -> list[float]:
|
||||
"""Average (arithmetic mean) pixel level for each band in the image."""
|
||||
return [self.sum[i] / self.count[i] for i in self.bands]
|
||||
return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
|
||||
|
||||
@cached_property
|
||||
def median(self) -> list[int]:
|
||||
|
@ -141,13 +141,20 @@ class Stat:
|
|||
@cached_property
|
||||
def rms(self) -> list[float]:
|
||||
"""RMS (root-mean-square) for each band in the image."""
|
||||
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
|
||||
return [
|
||||
math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
|
||||
for i in self.bands
|
||||
]
|
||||
|
||||
@cached_property
|
||||
def var(self) -> list[float]:
|
||||
"""Variance for each band in the image."""
|
||||
return [
|
||||
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
|
||||
(
|
||||
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
|
||||
if self.count[i]
|
||||
else 0
|
||||
)
|
||||
for i in self.bands
|
||||
]
|
||||
|
||||
|
|
|
@ -34,10 +34,6 @@ def _i(c: bytes) -> int:
|
|||
return i32((b"\0\0\0\0" + c)[-4:])
|
||||
|
||||
|
||||
def _i8(c: int | bytes) -> int:
|
||||
return c if isinstance(c, int) else c[0]
|
||||
|
||||
|
||||
##
|
||||
# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
|
||||
# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
|
||||
|
@ -100,16 +96,18 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
# mode
|
||||
layers = self.info[(3, 60)][0]
|
||||
component = self.info[(3, 60)][1]
|
||||
if (3, 65) in self.info:
|
||||
id = self.info[(3, 65)][0] - 1
|
||||
else:
|
||||
id = 0
|
||||
if layers == 1 and not component:
|
||||
self._mode = "L"
|
||||
elif layers == 3 and component:
|
||||
self._mode = "RGB"[id]
|
||||
elif layers == 4 and component:
|
||||
self._mode = "CMYK"[id]
|
||||
band = None
|
||||
else:
|
||||
if layers == 3 and component:
|
||||
self._mode = "RGB"
|
||||
elif layers == 4 and component:
|
||||
self._mode = "CMYK"
|
||||
if (3, 65) in self.info:
|
||||
band = self.info[(3, 65)][0] - 1
|
||||
else:
|
||||
band = 0
|
||||
|
||||
# size
|
||||
self._size = self.getint((3, 20)), self.getint((3, 30))
|
||||
|
@ -124,39 +122,44 @@ class IptcImageFile(ImageFile.ImageFile):
|
|||
# tile
|
||||
if tag == (8, 10):
|
||||
self.tile = [
|
||||
ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
|
||||
ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
|
||||
]
|
||||
|
||||
def load(self) -> Image.core.PixelAccess | None:
|
||||
if len(self.tile) != 1 or self.tile[0][0] != "iptc":
|
||||
return ImageFile.ImageFile.load(self)
|
||||
if self.tile:
|
||||
args = self.tile[0].args
|
||||
assert isinstance(args, tuple)
|
||||
compression, band = args
|
||||
|
||||
offset, compression = self.tile[0][2:]
|
||||
self.fp.seek(self.tile[0].offset)
|
||||
|
||||
self.fp.seek(offset)
|
||||
|
||||
# Copy image data to temporary file
|
||||
o = BytesIO()
|
||||
if compression == "raw":
|
||||
# To simplify access to the extracted file,
|
||||
# prepend a PPM header
|
||||
o.write(b"P5\n%d %d\n255\n" % self.size)
|
||||
while True:
|
||||
type, size = self.field()
|
||||
if type != (8, 10):
|
||||
break
|
||||
while size > 0:
|
||||
s = self.fp.read(min(size, 8192))
|
||||
if not s:
|
||||
# Copy image data to temporary file
|
||||
o = BytesIO()
|
||||
if compression == "raw":
|
||||
# To simplify access to the extracted file,
|
||||
# prepend a PPM header
|
||||
o.write(b"P5\n%d %d\n255\n" % self.size)
|
||||
while True:
|
||||
type, size = self.field()
|
||||
if type != (8, 10):
|
||||
break
|
||||
o.write(s)
|
||||
size -= len(s)
|
||||
while size > 0:
|
||||
s = self.fp.read(min(size, 8192))
|
||||
if not s:
|
||||
break
|
||||
o.write(s)
|
||||
size -= len(s)
|
||||
|
||||
with Image.open(o) as _im:
|
||||
_im.load()
|
||||
self.im = _im.im
|
||||
self.tile = []
|
||||
return Image.Image.load(self)
|
||||
with Image.open(o) as _im:
|
||||
if band is not None:
|
||||
bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
|
||||
bands[band] = _im
|
||||
_im = Image.merge(self.mode, bands)
|
||||
else:
|
||||
_im.load()
|
||||
self.im = _im.im
|
||||
self.tile = []
|
||||
return ImageFile.ImageFile.load(self)
|
||||
|
||||
|
||||
Image.register_open(IptcImageFile.format, IptcImageFile)
|
||||
|
|