mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-04 20:03:20 +03:00
Merge branch 'main' into image_grab_wayland_kde
This commit is contained in:
commit
2b62c0beeb
|
@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||||
sway wl-clipboard libopenblas-dev
|
sway wl-clipboard libopenblas-dev nasm
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
|
@ -36,6 +36,9 @@ python3 -m pip install -U pytest
|
||||||
python3 -m pip install -U pytest-cov
|
python3 -m pip install -U pytest-cov
|
||||||
python3 -m pip install -U pytest-timeout
|
python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
# optional test dependency, only install if there's a binary package.
|
||||||
|
# fails on beta 3.14 and PyPy
|
||||||
|
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
python3 -m pip install numpy
|
python3 -m pip install numpy
|
||||||
|
@ -62,6 +65,9 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
# raqm
|
# raqm
|
||||||
pushd depends && ./install_raqm.sh && popd
|
pushd depends && ./install_raqm.sh && popd
|
||||||
|
|
||||||
|
# libavif
|
||||||
|
pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
else
|
else
|
||||||
|
|
10
.github/workflows/macos-install.sh
vendored
10
.github/workflows/macos-install.sh
vendored
|
@ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then
|
||||||
brew uninstall gradle maven
|
brew uninstall gradle maven
|
||||||
fi
|
fi
|
||||||
brew install \
|
brew install \
|
||||||
|
aom \
|
||||||
|
dav1d \
|
||||||
freetype \
|
freetype \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
jpeg-turbo \
|
jpeg-turbo \
|
||||||
|
@ -14,6 +16,8 @@ brew install \
|
||||||
libtiff \
|
libtiff \
|
||||||
little-cms2 \
|
little-cms2 \
|
||||||
openjpeg \
|
openjpeg \
|
||||||
|
rav1e \
|
||||||
|
svt-av1 \
|
||||||
webp
|
webp
|
||||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
|
||||||
|
@ -26,6 +30,12 @@ python3 -m pip install -U pytest-cov
|
||||||
python3 -m pip install -U pytest-timeout
|
python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
python3 -m pip install numpy
|
python3 -m pip install numpy
|
||||||
|
# optional test dependency, only install if there's a binary package.
|
||||||
|
# fails on beta 3.14 and PyPy
|
||||||
|
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||||
|
|
||||||
|
# libavif
|
||||||
|
pushd depends && ./install_libavif.sh && popd
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
1
.github/workflows/test-mingw.yml
vendored
1
.github/workflows/test-mingw.yml
vendored
|
@ -60,6 +60,7 @@ jobs:
|
||||||
mingw-w64-x86_64-gcc \
|
mingw-w64-x86_64-gcc \
|
||||||
mingw-w64-x86_64-ghostscript \
|
mingw-w64-x86_64-ghostscript \
|
||||||
mingw-w64-x86_64-lcms2 \
|
mingw-w64-x86_64-lcms2 \
|
||||||
|
mingw-w64-x86_64-libavif \
|
||||||
mingw-w64-x86_64-libimagequant \
|
mingw-w64-x86_64-libimagequant \
|
||||||
mingw-w64-x86_64-libjpeg-turbo \
|
mingw-w64-x86_64-libjpeg-turbo \
|
||||||
mingw-w64-x86_64-libraqm \
|
mingw-w64-x86_64-libraqm \
|
||||||
|
|
10
.github/workflows/test-windows.yml
vendored
10
.github/workflows/test-windows.yml
vendored
|
@ -42,7 +42,7 @@ jobs:
|
||||||
# Test the oldest Python on 32-bit
|
# Test the oldest Python on 32-bit
|
||||||
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
|
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 45
|
||||||
|
|
||||||
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
|
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
|
||||||
|
|
||||||
|
@ -88,6 +88,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install PyQt6
|
python3 -m pip install PyQt6
|
||||||
|
|
||||||
|
- name: Install PyArrow dependency
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --only-binary=:all: pyarrow || true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: install
|
id: install
|
||||||
run: |
|
run: |
|
||||||
|
@ -145,6 +149,10 @@ jobs:
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
run: "& winbuild\\build\\build_dep_libpng.cmd"
|
run: "& winbuild\\build\\build_dep_libpng.cmd"
|
||||||
|
|
||||||
|
- name: Build dependencies / libavif
|
||||||
|
if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64'
|
||||||
|
run: "& winbuild\\build\\build_dep_libavif.cmd"
|
||||||
|
|
||||||
# for FreeType WOFF2 font support
|
# for FreeType WOFF2 font support
|
||||||
- name: Build dependencies / brotli
|
- name: Build dependencies / brotli
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
if: steps.build-cache.outputs.cache-hit != 'true'
|
||||||
|
|
68
.github/workflows/wheels-dependencies.sh
vendored
68
.github/workflows/wheels-dependencies.sh
vendored
|
@ -25,7 +25,7 @@ else
|
||||||
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
|
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
|
||||||
MB_ML_VER=${AUDITWHEEL_POLICY:9}
|
MB_ML_VER=${AUDITWHEEL_POLICY:9}
|
||||||
fi
|
fi
|
||||||
PLAT=$CIBW_ARCHS
|
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
|
||||||
|
|
||||||
# Define custom utilities
|
# Define custom utilities
|
||||||
source wheels/multibuild/common_utils.sh
|
source wheels/multibuild/common_utils.sh
|
||||||
|
@ -42,18 +42,30 @@ HARFBUZZ_VERSION=11.0.0
|
||||||
LIBPNG_VERSION=1.6.47
|
LIBPNG_VERSION=1.6.47
|
||||||
JPEGTURBO_VERSION=3.1.0
|
JPEGTURBO_VERSION=3.1.0
|
||||||
OPENJPEG_VERSION=2.5.3
|
OPENJPEG_VERSION=2.5.3
|
||||||
if [[ $MB_ML_VER == 2014 ]]; then
|
XZ_VERSION=5.8.0
|
||||||
XZ_VERSION=5.6.4
|
|
||||||
else
|
|
||||||
XZ_VERSION=5.8.0
|
|
||||||
fi
|
|
||||||
TIFF_VERSION=4.7.0
|
TIFF_VERSION=4.7.0
|
||||||
LCMS2_VERSION=2.17
|
LCMS2_VERSION=2.17
|
||||||
|
ZLIB_VERSION=1.3.1
|
||||||
ZLIB_NG_VERSION=2.2.4
|
ZLIB_NG_VERSION=2.2.4
|
||||||
LIBWEBP_VERSION=1.5.0
|
LIBWEBP_VERSION=1.5.0
|
||||||
BZIP2_VERSION=1.0.8
|
BZIP2_VERSION=1.0.8
|
||||||
LIBXCB_VERSION=1.17.0
|
LIBXCB_VERSION=1.17.0
|
||||||
BROTLI_VERSION=1.1.0
|
BROTLI_VERSION=1.1.0
|
||||||
|
LIBAVIF_VERSION=1.2.1
|
||||||
|
|
||||||
|
if [[ $MB_ML_VER == 2014 ]]; then
|
||||||
|
function build_xz {
|
||||||
|
if [ -e xz-stamp ]; then return; fi
|
||||||
|
yum install -y gettext-devel
|
||||||
|
fetch_unpack https://tukaani.org/xz/xz-$XZ_VERSION.tar.gz
|
||||||
|
(cd xz-$XZ_VERSION \
|
||||||
|
&& ./autogen.sh --no-po4a \
|
||||||
|
&& ./configure --prefix=$BUILD_PREFIX \
|
||||||
|
&& make -j4 \
|
||||||
|
&& make install)
|
||||||
|
touch xz-stamp
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
function build_pkg_config {
|
function build_pkg_config {
|
||||||
if [ -e pkg-config-stamp ]; then return; fi
|
if [ -e pkg-config-stamp ]; then return; fi
|
||||||
|
@ -105,12 +117,55 @@ function build_harfbuzz {
|
||||||
touch harfbuzz-stamp
|
touch harfbuzz-stamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function build_libavif {
|
||||||
|
if [ -e libavif-stamp ]; then return; fi
|
||||||
|
|
||||||
|
python3 -m pip install meson ninja
|
||||||
|
|
||||||
|
if [[ "$PLAT" == "x86_64" ]] || [ -n "$SANITIZER" ]; then
|
||||||
|
build_simple nasm 2.16.03 https://www.nasm.us/pub/nasm/releasebuilds/2.16.03
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For rav1e
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
. "$HOME/.cargo/env"
|
||||||
|
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||||
|
yum install -y perl
|
||||||
|
if [[ "$MB_ML_VER" == 2014 ]]; then
|
||||||
|
yum install -y perl-IPC-Cmd
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local out_dir=$(fetch_unpack https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$LIBAVIF_VERSION.tar.gz libavif-$LIBAVIF_VERSION.tar.gz)
|
||||||
|
(cd $out_dir \
|
||||||
|
&& CMAKE_POLICY_VERSION_MINIMUM=3.5 cmake \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX \
|
||||||
|
-DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DAVIF_LIBSHARPYUV=LOCAL \
|
||||||
|
-DAVIF_LIBYUV=LOCAL \
|
||||||
|
-DAVIF_CODEC_AOM=LOCAL \
|
||||||
|
-DAVIF_CODEC_DAV1D=LOCAL \
|
||||||
|
-DAVIF_CODEC_RAV1E=LOCAL \
|
||||||
|
-DAVIF_CODEC_SVT=LOCAL \
|
||||||
|
-DENABLE_NASM=ON \
|
||||||
|
-DCMAKE_MODULE_PATH=/tmp/cmake/Modules \
|
||||||
|
. \
|
||||||
|
&& make install)
|
||||||
|
touch libavif-stamp
|
||||||
|
}
|
||||||
|
|
||||||
function build {
|
function build {
|
||||||
build_xz
|
build_xz
|
||||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||||
yum remove -y zlib-devel
|
yum remove -y zlib-devel
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
|
||||||
|
build_new_zlib
|
||||||
|
else
|
||||||
build_zlib_ng
|
build_zlib_ng
|
||||||
|
fi
|
||||||
|
|
||||||
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||||
if [ -n "$IS_MACOS" ]; then
|
if [ -n "$IS_MACOS" ]; then
|
||||||
|
@ -135,6 +190,7 @@ function build {
|
||||||
build_tiff
|
build_tiff
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
build_libavif
|
||||||
build_libpng
|
build_libpng
|
||||||
build_lcms2
|
build_lcms2
|
||||||
build_openjpeg
|
build_openjpeg
|
||||||
|
|
5
.github/workflows/wheels.yml
vendored
5
.github/workflows/wheels.yml
vendored
|
@ -160,6 +160,11 @@ jobs:
|
||||||
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
|
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Update rust
|
||||||
|
if: matrix.cibw_arch == 'AMD64'
|
||||||
|
run: |
|
||||||
|
rustup update
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
run: |
|
run: |
|
||||||
setlocal EnableDelayedExpansion
|
setlocal EnableDelayedExpansion
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import struct
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PIL import features
|
from PIL import features
|
||||||
|
|
||||||
|
from .helper import is_pypy
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_modules() -> None:
|
def test_wheel_modules() -> None:
|
||||||
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
|
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp", "avif"}
|
||||||
|
|
||||||
# tkinter is not available in cibuildwheel installed CPython on Windows
|
# tkinter is not available in cibuildwheel installed CPython on Windows
|
||||||
try:
|
try:
|
||||||
|
@ -16,6 +20,11 @@ def test_wheel_modules() -> None:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
expected_modules.remove("tkinter")
|
expected_modules.remove("tkinter")
|
||||||
|
|
||||||
|
# libavif is not available on Windows for x86 and ARM64 architectures
|
||||||
|
if sys.platform == "win32":
|
||||||
|
if platform.machine() == "ARM64" or struct.calcsize("P") == 4:
|
||||||
|
expected_modules.remove("avif")
|
||||||
|
|
||||||
assert set(features.get_supported_modules()) == expected_modules
|
assert set(features.get_supported_modules()) == expected_modules
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,5 +49,7 @@ def test_wheel_features() -> None:
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
expected_features.remove("xcb")
|
expected_features.remove("xcb")
|
||||||
|
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
|
||||||
|
expected_features.remove("zlib_ng")
|
||||||
|
|
||||||
assert set(features.get_supported_features()) == expected_features
|
assert set(features.get_supported_features()) == expected_features
|
||||||
|
|
BIN
Tests/images/avif/exif.avif
Normal file
BIN
Tests/images/avif/exif.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper-missing-pixi.avif
Normal file
BIN
Tests/images/avif/hopper-missing-pixi.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper.avif
Normal file
BIN
Tests/images/avif/hopper.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper.heif
Normal file
BIN
Tests/images/avif/hopper.heif
Normal file
Binary file not shown.
BIN
Tests/images/avif/hopper_avif_write.png
Normal file
BIN
Tests/images/avif/hopper_avif_write.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
Tests/images/avif/icc_profile.avif
Normal file
BIN
Tests/images/avif/icc_profile.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/icc_profile_none.avif
Normal file
BIN
Tests/images/avif/icc_profile_none.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot0mir0.avif
Normal file
BIN
Tests/images/avif/rot0mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot0mir1.avif
Normal file
BIN
Tests/images/avif/rot0mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir0.avif
Normal file
BIN
Tests/images/avif/rot1mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot1mir1.avif
Normal file
BIN
Tests/images/avif/rot1mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir0.avif
Normal file
BIN
Tests/images/avif/rot2mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot2mir1.avif
Normal file
BIN
Tests/images/avif/rot2mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir0.avif
Normal file
BIN
Tests/images/avif/rot3mir0.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/rot3mir1.avif
Normal file
BIN
Tests/images/avif/rot3mir1.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/star.avifs
Normal file
BIN
Tests/images/avif/star.avifs
Normal file
Binary file not shown.
BIN
Tests/images/avif/star.gif
Normal file
BIN
Tests/images/avif/star.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
Tests/images/avif/star.png
Normal file
BIN
Tests/images/avif/star.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
Tests/images/avif/transparency.avif
Normal file
BIN
Tests/images/avif/transparency.avif
Normal file
Binary file not shown.
BIN
Tests/images/avif/xmp_tags_orientation.avif
Normal file
BIN
Tests/images/avif/xmp_tags_orientation.avif
Normal file
Binary file not shown.
164
Tests/test_arrow.py
Normal file
164
Tests/test_arrow.py
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from .helper import hopper
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode, dest_modes",
|
||||||
|
(
|
||||||
|
("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]),
|
||||||
|
("I", ["L", "F"]), # Technically I;32 can work for any 4x8bit storage.
|
||||||
|
("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]),
|
||||||
|
("LA", ["L", "F"]),
|
||||||
|
("RGB", ["L", "F"]),
|
||||||
|
("RGBA", ["L", "F"]),
|
||||||
|
("RGBX", ["L", "F"]),
|
||||||
|
("CMYK", ["L", "F"]),
|
||||||
|
("YCbCr", ["L", "F"]),
|
||||||
|
("HSV", ["L", "F"]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None:
|
||||||
|
img = hopper(mode)
|
||||||
|
for dest_mode in dest_modes:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Image.fromarrow(img, dest_mode, img.size)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_array_size() -> None:
|
||||||
|
img = hopper("RGB")
|
||||||
|
|
||||||
|
assert img.size != (10, 10)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Image.fromarrow(img, "RGB", (10, 10))
|
||||||
|
|
||||||
|
|
||||||
|
def test_release_schema() -> None:
|
||||||
|
# these should not error out, valgrind should be clean
|
||||||
|
img = hopper("L")
|
||||||
|
schema = img.__arrow_c_schema__()
|
||||||
|
del schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_release_array() -> None:
|
||||||
|
# these should not error out, valgrind should be clean
|
||||||
|
img = hopper("L")
|
||||||
|
array, schema = img.__arrow_c_array__()
|
||||||
|
del array
|
||||||
|
del schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_readonly() -> None:
|
||||||
|
img = hopper("L")
|
||||||
|
reloaded = Image.fromarrow(img, img.mode, img.size)
|
||||||
|
assert reloaded.readonly == 1
|
||||||
|
reloaded._readonly = 0
|
||||||
|
assert reloaded.readonly == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiblock_l_image() -> None:
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in single channel mode
|
||||||
|
size = (4096, 2 * block_size // 4096)
|
||||||
|
img = Image.new("L", size, 128)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
(schema, arr) = img.__arrow_c_array__()
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiblock_rgba_image() -> None:
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in 4 channel mode
|
||||||
|
size = (4096, (block_size // 4096) // 2)
|
||||||
|
img = Image.new("RGBA", size, (128, 127, 126, 125))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
(schema, arr) = img.__arrow_c_array__()
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiblock_l_schema() -> None:
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in single channel mode
|
||||||
|
size = (4096, 2 * block_size // 4096)
|
||||||
|
img = Image.new("L", size, 128)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
img.__arrow_c_schema__()
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiblock_rgba_schema() -> None:
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in 4 channel mode
|
||||||
|
size = (4096, (block_size // 4096) // 2)
|
||||||
|
img = Image.new("RGBA", size, (128, 127, 126, 125))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
img.__arrow_c_schema__()
|
||||||
|
|
||||||
|
|
||||||
|
def test_singleblock_l_image() -> None:
|
||||||
|
Image.core.set_use_block_allocator(1)
|
||||||
|
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in 4 channel mode
|
||||||
|
size = (4096, 2 * (block_size // 4096))
|
||||||
|
img = Image.new("L", size, 128)
|
||||||
|
assert img.im.isblock()
|
||||||
|
|
||||||
|
(schema, arr) = img.__arrow_c_array__()
|
||||||
|
assert schema
|
||||||
|
assert arr
|
||||||
|
|
||||||
|
Image.core.set_use_block_allocator(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_singleblock_rgba_image() -> None:
|
||||||
|
Image.core.set_use_block_allocator(1)
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in 4 channel mode
|
||||||
|
size = (4096, (block_size // 4096) // 2)
|
||||||
|
img = Image.new("RGBA", size, (128, 127, 126, 125))
|
||||||
|
assert img.im.isblock()
|
||||||
|
|
||||||
|
(schema, arr) = img.__arrow_c_array__()
|
||||||
|
assert schema
|
||||||
|
assert arr
|
||||||
|
Image.core.set_use_block_allocator(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_singleblock_l_schema() -> None:
|
||||||
|
Image.core.set_use_block_allocator(1)
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in single channel mode
|
||||||
|
size = (4096, 2 * block_size // 4096)
|
||||||
|
img = Image.new("L", size, 128)
|
||||||
|
assert img.im.isblock()
|
||||||
|
|
||||||
|
schema = img.__arrow_c_schema__()
|
||||||
|
assert schema
|
||||||
|
Image.core.set_use_block_allocator(0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_singleblock_rgba_schema() -> None:
|
||||||
|
Image.core.set_use_block_allocator(1)
|
||||||
|
block_size = Image.core.get_block_size()
|
||||||
|
|
||||||
|
# check a 2 block image in 4 channel mode
|
||||||
|
size = (4096, (block_size // 4096) // 2)
|
||||||
|
img = Image.new("RGBA", size, (128, 127, 126, 125))
|
||||||
|
assert img.im.isblock()
|
||||||
|
|
||||||
|
schema = img.__arrow_c_schema__()
|
||||||
|
assert schema
|
||||||
|
Image.core.set_use_block_allocator(0)
|
|
@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
|
||||||
# (referenced from https://wiki.mozilla.org/APNG_Specification)
|
# (referenced from https://wiki.mozilla.org/APNG_Specification)
|
||||||
def test_apng_basic() -> None:
|
def test_apng_basic() -> None:
|
||||||
with Image.open("Tests/images/apng/single_frame.png") as im:
|
with Image.open("Tests/images/apng/single_frame.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert im.get_format_mimetype() == "image/apng"
|
assert im.get_format_mimetype() == "image/apng"
|
||||||
|
@ -20,6 +21,7 @@ def test_apng_basic() -> None:
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/single_frame_default.png") as im:
|
with Image.open("Tests/images/apng/single_frame_default.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
assert im.get_format_mimetype() == "image/apng"
|
assert im.get_format_mimetype() == "image/apng"
|
||||||
|
@ -52,6 +54,7 @@ def test_apng_basic() -> None:
|
||||||
)
|
)
|
||||||
def test_apng_fdat(filename: str) -> None:
|
def test_apng_fdat(filename: str) -> None:
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
@ -59,31 +62,37 @@ def test_apng_fdat(filename: str) -> None:
|
||||||
|
|
||||||
def test_apng_dispose() -> None:
|
def test_apng_dispose() -> None:
|
||||||
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_background.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_background.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_background_final.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_background_final.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_previous.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_previous.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
@ -91,21 +100,25 @@ def test_apng_dispose() -> None:
|
||||||
|
|
||||||
def test_apng_dispose_region() -> None:
|
def test_apng_dispose_region() -> None:
|
||||||
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_background_region.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_background_region.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
|
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
@ -132,6 +145,7 @@ def test_apng_dispose_op_previous_frame() -> None:
|
||||||
# ],
|
# ],
|
||||||
# )
|
# )
|
||||||
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
|
with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
|
assert im.getpixel((0, 0)) == (255, 0, 0, 255)
|
||||||
|
|
||||||
|
@ -145,26 +159,31 @@ def test_apng_dispose_op_background_p_mode() -> None:
|
||||||
|
|
||||||
def test_apng_blend() -> None:
|
def test_apng_blend() -> None:
|
||||||
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
|
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im:
|
with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im:
|
with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 2)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 2)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 2)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 2)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/blend_op_over.png") as im:
|
with Image.open("Tests/images/apng/blend_op_over.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im:
|
with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 97)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 97)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
@ -178,6 +197,7 @@ def test_apng_blend_transparency() -> None:
|
||||||
|
|
||||||
def test_apng_chunk_order() -> None:
|
def test_apng_chunk_order() -> None:
|
||||||
with Image.open("Tests/images/apng/fctl_actl.png") as im:
|
with Image.open("Tests/images/apng/fctl_actl.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
@ -233,24 +253,28 @@ def test_apng_num_plays() -> None:
|
||||||
|
|
||||||
def test_apng_mode() -> None:
|
def test_apng_mode() -> None:
|
||||||
with Image.open("Tests/images/apng/mode_16bit.png") as im:
|
with Image.open("Tests/images/apng/mode_16bit.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.mode == "RGBA"
|
assert im.mode == "RGBA"
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
|
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
|
||||||
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
|
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
|
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.mode == "L"
|
assert im.mode == "L"
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == 128
|
assert im.getpixel((0, 0)) == 128
|
||||||
assert im.getpixel((64, 32)) == 255
|
assert im.getpixel((64, 32)) == 255
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
|
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.mode == "LA"
|
assert im.mode == "LA"
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
assert im.getpixel((0, 0)) == (128, 191)
|
assert im.getpixel((0, 0)) == (128, 191)
|
||||||
assert im.getpixel((64, 32)) == (128, 191)
|
assert im.getpixel((64, 32)) == (128, 191)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/mode_palette.png") as im:
|
with Image.open("Tests/images/apng/mode_palette.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im = im.convert("RGB")
|
im = im.convert("RGB")
|
||||||
|
@ -258,6 +282,7 @@ def test_apng_mode() -> None:
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0)
|
assert im.getpixel((64, 32)) == (0, 255, 0)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
|
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im = im.convert("RGBA")
|
im = im.convert("RGBA")
|
||||||
|
@ -265,6 +290,7 @@ def test_apng_mode() -> None:
|
||||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
|
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im = im.convert("RGBA")
|
im = im.convert("RGBA")
|
||||||
|
@ -274,25 +300,31 @@ def test_apng_mode() -> None:
|
||||||
|
|
||||||
def test_apng_chunk_errors() -> None:
|
def test_apng_chunk_errors() -> None:
|
||||||
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
|
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
|
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
|
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
|
||||||
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
|
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
|
||||||
|
@ -300,26 +332,31 @@ def test_apng_chunk_errors() -> None:
|
||||||
def test_apng_syntax_errors() -> None:
|
def test_apng_syntax_errors() -> None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
# we can handle this case gracefully
|
# we can handle this case gracefully
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
|
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -339,6 +376,7 @@ def test_apng_syntax_errors() -> None:
|
||||||
def test_apng_sequence_errors(test_file: str) -> None:
|
def test_apng_sequence_errors(test_file: str) -> None:
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -349,6 +387,7 @@ def test_apng_save(tmp_path: Path) -> None:
|
||||||
im.save(test_file, save_all=True)
|
im.save(test_file, save_all=True)
|
||||||
|
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.load()
|
im.load()
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
@ -364,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.load()
|
im.load()
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
|
@ -403,6 +443,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
|
||||||
append_images=frames,
|
append_images=frames,
|
||||||
)
|
)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
im.seek(im.n_frames - 1)
|
im.seek(im.n_frames - 1)
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
|
@ -445,6 +486,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
||||||
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
|
||||||
)
|
)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert "duration" not in im.info
|
assert "duration" not in im.info
|
||||||
|
|
||||||
|
@ -456,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
||||||
duration=[500, 100, 150],
|
duration=[500, 100, 150],
|
||||||
)
|
)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
assert im.info["duration"] == 600
|
assert im.info["duration"] == 600
|
||||||
|
|
||||||
|
@ -466,6 +509,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
||||||
frame.info["duration"] = 300
|
frame.info["duration"] = 300
|
||||||
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
|
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
assert im.info["duration"] == 600
|
assert im.info["duration"] == 600
|
||||||
|
|
||||||
|
|
778
Tests/test_file_avif.py
Normal file
778
Tests/test_file_avif.py
Normal file
|
@ -0,0 +1,778 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import gc
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
|
from collections.abc import Generator, Sequence
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PIL import (
|
||||||
|
AvifImagePlugin,
|
||||||
|
Image,
|
||||||
|
ImageDraw,
|
||||||
|
ImageFile,
|
||||||
|
UnidentifiedImageError,
|
||||||
|
features,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .helper import (
|
||||||
|
PillowLeakTestCase,
|
||||||
|
assert_image,
|
||||||
|
assert_image_similar,
|
||||||
|
assert_image_similar_tofile,
|
||||||
|
hopper,
|
||||||
|
skip_unless_feature,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import _avif
|
||||||
|
|
||||||
|
HAVE_AVIF = True
|
||||||
|
except ImportError:
|
||||||
|
HAVE_AVIF = False
|
||||||
|
|
||||||
|
|
||||||
|
TEST_AVIF_FILE = "Tests/images/avif/hopper.avif"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_xmp_orientation(xmp: bytes, expected: int) -> None:
|
||||||
|
assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile:
|
||||||
|
out = BytesIO()
|
||||||
|
im.save(out, "AVIF", **options)
|
||||||
|
return Image.open(out)
|
||||||
|
|
||||||
|
|
||||||
|
def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator:
|
||||||
|
reason = f"{codec_name} decode not available"
|
||||||
|
return pytest.mark.skipif(
|
||||||
|
not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator:
|
||||||
|
reason = f"{codec_name} encode not available"
|
||||||
|
return pytest.mark.skipif(
|
||||||
|
not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_docker_qemu() -> bool:
|
||||||
|
try:
|
||||||
|
init_proc_exe = os.readlink("/proc/1/exe")
|
||||||
|
except (FileNotFoundError, PermissionError):
|
||||||
|
return False
|
||||||
|
return "qemu" in init_proc_exe
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnsupportedAvif:
|
||||||
|
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
|
||||||
|
|
||||||
|
with pytest.warns(UserWarning):
|
||||||
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
with Image.open(TEST_AVIF_FILE):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("avif")
|
||||||
|
class TestFileAvif:
|
||||||
|
def test_version(self) -> None:
|
||||||
|
version = features.version_module("avif")
|
||||||
|
assert version is not None
|
||||||
|
assert re.search(r"^\d+\.\d+\.\d+$", version)
|
||||||
|
|
||||||
|
def test_codec_version(self) -> None:
|
||||||
|
assert AvifImagePlugin.get_codec_version("unknown") is None
|
||||||
|
|
||||||
|
for codec_name in ("aom", "dav1d", "rav1e", "svt"):
|
||||||
|
codec_version = AvifImagePlugin.get_codec_version(codec_name)
|
||||||
|
if _avif.decoder_codec_available(
|
||||||
|
codec_name
|
||||||
|
) or _avif.encoder_codec_available(codec_name):
|
||||||
|
assert codec_version is not None
|
||||||
|
assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version)
|
||||||
|
else:
|
||||||
|
assert codec_version is None
|
||||||
|
|
||||||
|
def test_read(self) -> None:
|
||||||
|
"""
|
||||||
|
Can we read an AVIF file without error?
|
||||||
|
Does it have the bits we expect?
|
||||||
|
"""
|
||||||
|
|
||||||
|
with Image.open(TEST_AVIF_FILE) as image:
|
||||||
|
assert image.mode == "RGB"
|
||||||
|
assert image.size == (128, 128)
|
||||||
|
assert image.format == "AVIF"
|
||||||
|
assert image.get_format_mimetype() == "image/avif"
|
||||||
|
image.getdata()
|
||||||
|
|
||||||
|
# generated with:
|
||||||
|
# avifdec hopper.avif hopper_avif_write.png
|
||||||
|
assert_image_similar_tofile(
|
||||||
|
image, "Tests/images/avif/hopper_avif_write.png", 11.5
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_write_rgb(self, tmp_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
Can we write a RGB mode file to avif without error?
|
||||||
|
Does it have the bits we expect?
|
||||||
|
"""
|
||||||
|
|
||||||
|
temp_file = tmp_path / "temp.avif"
|
||||||
|
|
||||||
|
im = hopper()
|
||||||
|
im.save(temp_file)
|
||||||
|
with Image.open(temp_file) as reloaded:
|
||||||
|
assert reloaded.mode == "RGB"
|
||||||
|
assert reloaded.size == (128, 128)
|
||||||
|
assert reloaded.format == "AVIF"
|
||||||
|
reloaded.getdata()
|
||||||
|
|
||||||
|
# avifdec hopper.avif avif/hopper_avif_write.png
|
||||||
|
assert_image_similar_tofile(
|
||||||
|
reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02
|
||||||
|
)
|
||||||
|
|
||||||
|
# This test asserts that the images are similar. If the average pixel
|
||||||
|
# difference between the two images is less than the epsilon value,
|
||||||
|
# then we're going to accept that it's a reasonable lossy version of
|
||||||
|
# the image.
|
||||||
|
assert_image_similar(reloaded, im, 8.62)
|
||||||
|
|
||||||
|
def test_AvifEncoder_with_invalid_args(self) -> None:
|
||||||
|
"""
|
||||||
|
Calling encoder functions with no arguments should result in an error.
|
||||||
|
"""
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
_avif.AvifEncoder()
|
||||||
|
|
||||||
|
def test_AvifDecoder_with_invalid_args(self) -> None:
|
||||||
|
"""
|
||||||
|
Calling decoder functions with no arguments should result in an error.
|
||||||
|
"""
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
_avif.AvifDecoder()
|
||||||
|
|
||||||
|
def test_invalid_dimensions(self, tmp_path: Path) -> None:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im = Image.new("RGB", (0, 0))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(test_file)
|
||||||
|
|
||||||
|
def test_encoder_finish_none_error(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""Save should raise an OSError if AvifEncoder.finish returns None"""
|
||||||
|
|
||||||
|
class _mock_avif:
|
||||||
|
class AvifEncoder:
|
||||||
|
def __init__(self, *args: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add(self, *args: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finish(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif)
|
||||||
|
|
||||||
|
im = Image.new("RGB", (150, 150))
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
im.save(test_file)
|
||||||
|
|
||||||
|
def test_no_resource_warning(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("error")
|
||||||
|
|
||||||
|
im.save(tmp_path / "temp.avif")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"])
|
||||||
|
def test_accept_ftyp_brands(self, major_brand: bytes) -> None:
|
||||||
|
data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand
|
||||||
|
assert AvifImagePlugin._accept(data) is True
|
||||||
|
|
||||||
|
def test_file_pointer_could_be_reused(self) -> None:
|
||||||
|
with open(TEST_AVIF_FILE, "rb") as blob:
|
||||||
|
with Image.open(blob) as im:
|
||||||
|
im.load()
|
||||||
|
with Image.open(blob) as im:
|
||||||
|
im.load()
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
# Save as AVIF
|
||||||
|
out_avif = tmp_path / "temp.avif"
|
||||||
|
im.save(out_avif, save_all=True)
|
||||||
|
|
||||||
|
# Save as GIF
|
||||||
|
out_gif = tmp_path / "temp.gif"
|
||||||
|
with Image.open(out_avif) as im:
|
||||||
|
im.save(out_gif)
|
||||||
|
|
||||||
|
with Image.open(out_gif) as reread:
|
||||||
|
reread_value = reread.convert("RGB").getpixel((1, 1))
|
||||||
|
difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)])
|
||||||
|
assert difference <= 3
|
||||||
|
|
||||||
|
def test_save_single_frame(self, tmp_path: Path) -> None:
|
||||||
|
temp_file = tmp_path / "temp.avif"
|
||||||
|
with Image.open("Tests/images/chi.gif") as im:
|
||||||
|
im.save(temp_file)
|
||||||
|
with Image.open(temp_file) as im:
|
||||||
|
assert im.n_frames == 1
|
||||||
|
|
||||||
|
def test_invalid_file(self) -> None:
|
||||||
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
|
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
AvifImagePlugin.AvifImageFile(invalid_file)
|
||||||
|
|
||||||
|
def test_load_transparent_rgb(self) -> None:
|
||||||
|
test_file = "Tests/images/avif/transparency.avif"
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
assert_image(im, "RGBA", (64, 64))
|
||||||
|
|
||||||
|
# image has 876 transparent pixels
|
||||||
|
assert im.getchannel("A").getcolors()[0] == (876, 0)
|
||||||
|
|
||||||
|
def test_save_transparent(self, tmp_path: Path) -> None:
|
||||||
|
im = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
|
||||||
|
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||||
|
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file)
|
||||||
|
|
||||||
|
# check if saved image contains the same transparency
|
||||||
|
with Image.open(test_file) as im:
|
||||||
|
assert_image(im, "RGBA", (10, 10))
|
||||||
|
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||||
|
|
||||||
|
def test_save_icc_profile(self) -> None:
|
||||||
|
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
|
||||||
|
assert "icc_profile" not in im.info
|
||||||
|
|
||||||
|
with Image.open("Tests/images/avif/icc_profile.avif") as with_icc:
|
||||||
|
expected_icc = with_icc.info["icc_profile"]
|
||||||
|
assert expected_icc is not None
|
||||||
|
|
||||||
|
im = roundtrip(im, icc_profile=expected_icc)
|
||||||
|
assert im.info["icc_profile"] == expected_icc
|
||||||
|
|
||||||
|
def test_discard_icc_profile(self) -> None:
|
||||||
|
with Image.open("Tests/images/avif/icc_profile.avif") as im:
|
||||||
|
im = roundtrip(im, icc_profile=None)
|
||||||
|
assert "icc_profile" not in im.info
|
||||||
|
|
||||||
|
def test_roundtrip_icc_profile(self) -> None:
|
||||||
|
with Image.open("Tests/images/avif/icc_profile.avif") as im:
|
||||||
|
expected_icc = im.info["icc_profile"]
|
||||||
|
|
||||||
|
im = roundtrip(im)
|
||||||
|
assert im.info["icc_profile"] == expected_icc
|
||||||
|
|
||||||
|
def test_roundtrip_no_icc_profile(self) -> None:
|
||||||
|
with Image.open("Tests/images/avif/icc_profile_none.avif") as im:
|
||||||
|
assert "icc_profile" not in im.info
|
||||||
|
|
||||||
|
im = roundtrip(im)
|
||||||
|
assert "icc_profile" not in im.info
|
||||||
|
|
||||||
|
def test_exif(self) -> None:
|
||||||
|
# With an EXIF chunk
|
||||||
|
with Image.open("Tests/images/avif/exif.avif") as im:
|
||||||
|
exif = im.getexif()
|
||||||
|
assert exif[274] == 1
|
||||||
|
|
||||||
|
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
|
||||||
|
exif = im.getexif()
|
||||||
|
assert exif[274] == 3
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("use_bytes", [True, False])
|
||||||
|
@pytest.mark.parametrize("orientation", [1, 2])
|
||||||
|
def test_exif_save(
|
||||||
|
self,
|
||||||
|
tmp_path: Path,
|
||||||
|
use_bytes: bool,
|
||||||
|
orientation: int,
|
||||||
|
) -> None:
|
||||||
|
exif = Image.Exif()
|
||||||
|
exif[274] = orientation
|
||||||
|
exif_data = exif.tobytes()
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file, exif=exif_data if use_bytes else exif)
|
||||||
|
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
if orientation == 1:
|
||||||
|
assert "exif" not in reloaded.info
|
||||||
|
else:
|
||||||
|
assert reloaded.info["exif"] == exif_data
|
||||||
|
|
||||||
|
def test_exif_without_orientation(self, tmp_path: Path) -> None:
|
||||||
|
exif = Image.Exif()
|
||||||
|
exif[272] = b"test"
|
||||||
|
exif_data = exif.tobytes()
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file, exif=exif)
|
||||||
|
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert reloaded.info["exif"] == exif_data
|
||||||
|
|
||||||
|
def test_exif_invalid(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(SyntaxError):
|
||||||
|
im.save(test_file, exif=b"invalid")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"rot, mir, exif_orientation",
|
||||||
|
[
|
||||||
|
(0, 0, 4),
|
||||||
|
(0, 1, 2),
|
||||||
|
(1, 0, 5),
|
||||||
|
(1, 1, 7),
|
||||||
|
(2, 0, 2),
|
||||||
|
(2, 1, 4),
|
||||||
|
(3, 0, 7),
|
||||||
|
(3, 1, 5),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_rot_mir_exif(
|
||||||
|
self, rot: int, mir: int, exif_orientation: int, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im:
|
||||||
|
exif = im.getexif()
|
||||||
|
assert exif[274] == exif_orientation
|
||||||
|
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file, exif=exif)
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert reloaded.getexif()[274] == exif_orientation
|
||||||
|
|
||||||
|
def test_xmp(self) -> None:
|
||||||
|
with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im:
|
||||||
|
xmp = im.info["xmp"]
|
||||||
|
assert_xmp_orientation(xmp, 3)
|
||||||
|
|
||||||
|
def test_xmp_save(self, tmp_path: Path) -> None:
|
||||||
|
xmp_arg = "\n".join(
|
||||||
|
[
|
||||||
|
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>',
|
||||||
|
'<x:xmpmeta xmlns:x="adobe:ns:meta/">',
|
||||||
|
' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">',
|
||||||
|
' <rdf:Description rdf:about=""',
|
||||||
|
' xmlns:tiff="http://ns.adobe.com/tiff/1.0/"',
|
||||||
|
' tiff:Orientation="1"/>',
|
||||||
|
" </rdf:RDF>",
|
||||||
|
"</x:xmpmeta>",
|
||||||
|
'<?xpacket end="r"?>',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file, xmp=xmp_arg)
|
||||||
|
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
xmp = reloaded.info["xmp"]
|
||||||
|
assert_xmp_orientation(xmp, 1)
|
||||||
|
|
||||||
|
def test_tell(self) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
assert im.tell() == 0
|
||||||
|
|
||||||
|
def test_seek(self) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
im.seek(0)
|
||||||
|
|
||||||
|
with pytest.raises(EOFError):
|
||||||
|
im.seek(1)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"])
|
||||||
|
def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file, subsampling=subsampling)
|
||||||
|
|
||||||
|
def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(test_file, subsampling="foo")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ["full", "limited"])
|
||||||
|
def test_encoder_range(self, tmp_path: Path, value: str) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file, range=value)
|
||||||
|
|
||||||
|
def test_encoder_range_invalid(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(test_file, range="foo")
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("aom")
|
||||||
|
def test_encoder_codec_param(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
im.save(test_file, codec="aom")
|
||||||
|
|
||||||
|
def test_encoder_codec_invalid(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(test_file, codec="foo")
|
||||||
|
|
||||||
|
@skip_unless_avif_decoder("dav1d")
|
||||||
|
def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(test_file, codec="dav1d")
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("aom")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"advanced",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"aq-mode": "1",
|
||||||
|
"enable-chroma-deltaq": "1",
|
||||||
|
},
|
||||||
|
(("aq-mode", "1"), ("enable-chroma-deltaq", "1")),
|
||||||
|
[("aq-mode", "1"), ("enable-chroma-deltaq", "1")],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_encoder_advanced_codec_options(
|
||||||
|
self, advanced: dict[str, str] | Sequence[tuple[str, str]]
|
||||||
|
) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
ctrl_buf = BytesIO()
|
||||||
|
im.save(ctrl_buf, "AVIF", codec="aom")
|
||||||
|
test_buf = BytesIO()
|
||||||
|
im.save(
|
||||||
|
test_buf,
|
||||||
|
"AVIF",
|
||||||
|
codec="aom",
|
||||||
|
advanced=advanced,
|
||||||
|
)
|
||||||
|
assert ctrl_buf.getvalue() != test_buf.getvalue()
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("aom")
|
||||||
|
@pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234])
|
||||||
|
def test_encoder_advanced_codec_options_invalid(
|
||||||
|
self, tmp_path: Path, advanced: dict[str, str] | int
|
||||||
|
) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(test_file, codec="aom", advanced=advanced)
|
||||||
|
|
||||||
|
@skip_unless_avif_decoder("aom")
|
||||||
|
def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom")
|
||||||
|
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
assert im.size == (128, 128)
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("rav1e")
|
||||||
|
def test_encoder_codec_cannot_decode(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
with Image.open(TEST_AVIF_FILE):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
with Image.open(TEST_AVIF_FILE):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("aom")
|
||||||
|
def test_encoder_codec_available(self) -> None:
|
||||||
|
assert _avif.encoder_codec_available("aom") is True
|
||||||
|
|
||||||
|
def test_encoder_codec_available_bad_params(self) -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
_avif.encoder_codec_available()
|
||||||
|
|
||||||
|
@skip_unless_avif_decoder("dav1d")
|
||||||
|
def test_encoder_codec_available_cannot_decode(self) -> None:
|
||||||
|
assert _avif.encoder_codec_available("dav1d") is False
|
||||||
|
|
||||||
|
def test_encoder_codec_available_invalid(self) -> None:
|
||||||
|
assert _avif.encoder_codec_available("foo") is False
|
||||||
|
|
||||||
|
def test_encoder_quality_valueerror(self, tmp_path: Path) -> None:
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.save(test_file, quality="invalid")
|
||||||
|
|
||||||
|
@skip_unless_avif_decoder("aom")
|
||||||
|
def test_decoder_codec_available(self) -> None:
|
||||||
|
assert _avif.decoder_codec_available("aom") is True
|
||||||
|
|
||||||
|
def test_decoder_codec_available_bad_params(self) -> None:
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
_avif.decoder_codec_available()
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("rav1e")
|
||||||
|
def test_decoder_codec_available_cannot_decode(self) -> None:
|
||||||
|
assert _avif.decoder_codec_available("rav1e") is False
|
||||||
|
|
||||||
|
def test_decoder_codec_available_invalid(self) -> None:
|
||||||
|
assert _avif.decoder_codec_available("foo") is False
|
||||||
|
|
||||||
|
def test_p_mode_transparency(self, tmp_path: Path) -> None:
|
||||||
|
im = Image.new("P", size=(64, 64))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
draw.rectangle(xy=[(0, 0), (32, 32)], fill=255)
|
||||||
|
draw.rectangle(xy=[(32, 32), (64, 64)], fill=255)
|
||||||
|
|
||||||
|
out_png = tmp_path / "temp.png"
|
||||||
|
im.save(out_png, transparency=0)
|
||||||
|
with Image.open(out_png) as im_png:
|
||||||
|
out_avif = tmp_path / "temp.avif"
|
||||||
|
im_png.save(out_avif, quality=100)
|
||||||
|
|
||||||
|
with Image.open(out_avif) as expected:
|
||||||
|
assert_image_similar(im_png.convert("RGBA"), expected, 0.17)
|
||||||
|
|
||||||
|
def test_decoder_strict_flags(self) -> None:
|
||||||
|
# This would fail if full avif strictFlags were enabled
|
||||||
|
with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im:
|
||||||
|
assert im.size == (128, 128)
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("aom")
|
||||||
|
@pytest.mark.parametrize("speed", [-1, 1, 11])
|
||||||
|
def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
hopper().save(test_file, codec="aom", speed=speed)
|
||||||
|
|
||||||
|
@skip_unless_avif_encoder("svt")
|
||||||
|
def test_svt_optimizations(self, tmp_path: Path) -> None:
|
||||||
|
test_file = tmp_path / "temp.avif"
|
||||||
|
hopper().save(test_file, codec="svt", speed=1)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("avif")
|
||||||
|
class TestAvifAnimation:
|
||||||
|
@contextmanager
|
||||||
|
def star_frames(self) -> Generator[list[Image.Image], None, None]:
|
||||||
|
with Image.open("Tests/images/avif/star.png") as f:
|
||||||
|
yield [f, f.rotate(90), f.rotate(180), f.rotate(270)]
|
||||||
|
|
||||||
|
def test_n_frames(self) -> None:
|
||||||
|
"""
|
||||||
|
Ensure that AVIF format sets n_frames and is_animated attributes
|
||||||
|
correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with Image.open(TEST_AVIF_FILE) as im:
|
||||||
|
assert im.n_frames == 1
|
||||||
|
assert not im.is_animated
|
||||||
|
|
||||||
|
with Image.open("Tests/images/avif/star.avifs") as im:
|
||||||
|
assert im.n_frames == 5
|
||||||
|
assert im.is_animated
|
||||||
|
|
||||||
|
def test_write_animation_P(self, tmp_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
Convert an animated GIF to animated AVIF, then compare the frame
|
||||||
|
count, and ensure the frames are visually similar to the originals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with Image.open("Tests/images/avif/star.gif") as original:
|
||||||
|
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 im.n_frames == original.n_frames
|
||||||
|
|
||||||
|
# Compare first frame in P mode to frame from original GIF
|
||||||
|
assert_image_similar(im, original.convert("RGBA"), 2)
|
||||||
|
|
||||||
|
# Compare later frames in RGBA mode to frames from original GIF
|
||||||
|
for frame in range(1, original.n_frames):
|
||||||
|
original.seek(frame)
|
||||||
|
im.seek(frame)
|
||||||
|
assert_image_similar(im, original, 2.54)
|
||||||
|
|
||||||
|
def test_write_animation_RGBA(self, tmp_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
Write an animated AVIF from RGBA frames, and ensure the frames
|
||||||
|
are visually similar to the originals.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def check(temp_file: Path) -> None:
|
||||||
|
with Image.open(temp_file) as im:
|
||||||
|
assert im.n_frames == 4
|
||||||
|
|
||||||
|
# Compare first frame to original
|
||||||
|
assert_image_similar(im, frame1, 2.7)
|
||||||
|
|
||||||
|
# Compare second frame to original
|
||||||
|
im.seek(1)
|
||||||
|
assert_image_similar(im, frame2, 4.1)
|
||||||
|
|
||||||
|
with self.star_frames() as frames:
|
||||||
|
frame1 = frames[0]
|
||||||
|
frame2 = frames[1]
|
||||||
|
temp_file1 = tmp_path / "temp.avif"
|
||||||
|
frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:])
|
||||||
|
check(temp_file1)
|
||||||
|
|
||||||
|
# Test appending using a generator
|
||||||
|
def imGenerator(
|
||||||
|
ims: list[Image.Image],
|
||||||
|
) -> Generator[Image.Image, None, None]:
|
||||||
|
yield from ims
|
||||||
|
|
||||||
|
temp_file2 = tmp_path / "temp_generator.avif"
|
||||||
|
frames[0].copy().save(
|
||||||
|
temp_file2,
|
||||||
|
save_all=True,
|
||||||
|
append_images=imGenerator(frames[1:]),
|
||||||
|
)
|
||||||
|
check(temp_file2)
|
||||||
|
|
||||||
|
def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None:
|
||||||
|
temp_file = tmp_path / "temp.avif"
|
||||||
|
frame1 = Image.new("RGB", (100, 100))
|
||||||
|
frame2 = Image.new("RGB", (150, 150))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
frame1.save(temp_file, save_all=True, append_images=[frame2])
|
||||||
|
|
||||||
|
def test_heif_raises_unidentified_image_error(self) -> None:
|
||||||
|
with pytest.raises(UnidentifiedImageError):
|
||||||
|
with Image.open("Tests/images/avif/hopper.heif"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("alpha_premultiplied", [False, True])
|
||||||
|
def test_alpha_premultiplied(
|
||||||
|
self, tmp_path: Path, alpha_premultiplied: bool
|
||||||
|
) -> None:
|
||||||
|
temp_file = tmp_path / "temp.avif"
|
||||||
|
color = (200, 200, 200, 1)
|
||||||
|
im = Image.new("RGBA", (1, 1), color)
|
||||||
|
im.save(temp_file, alpha_premultiplied=alpha_premultiplied)
|
||||||
|
|
||||||
|
expected = (255, 255, 255, 1) if alpha_premultiplied else color
|
||||||
|
with Image.open(temp_file) as reloaded:
|
||||||
|
assert reloaded.getpixel((0, 0)) == expected
|
||||||
|
|
||||||
|
def test_timestamp_and_duration(self, tmp_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
Try passing a list of durations, and make sure the encoded
|
||||||
|
timestamps and durations are correct.
|
||||||
|
"""
|
||||||
|
|
||||||
|
durations = [1, 10, 20, 30, 40]
|
||||||
|
temp_file = tmp_path / "temp.avif"
|
||||||
|
with self.star_frames() as frames:
|
||||||
|
frames[0].save(
|
||||||
|
temp_file,
|
||||||
|
save_all=True,
|
||||||
|
append_images=(frames[1:] + [frames[0]]),
|
||||||
|
duration=durations,
|
||||||
|
)
|
||||||
|
|
||||||
|
with Image.open(temp_file) as im:
|
||||||
|
assert im.n_frames == 5
|
||||||
|
assert im.is_animated
|
||||||
|
|
||||||
|
# Check that timestamps and durations match original values specified
|
||||||
|
timestamp = 0
|
||||||
|
for frame in range(im.n_frames):
|
||||||
|
im.seek(frame)
|
||||||
|
im.load()
|
||||||
|
assert im.info["duration"] == durations[frame]
|
||||||
|
assert im.info["timestamp"] == timestamp
|
||||||
|
timestamp += durations[frame]
|
||||||
|
|
||||||
|
def test_seeking(self, tmp_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
Create an animated AVIF file, and then try seeking through frames in
|
||||||
|
reverse-order, verifying the timestamps and durations are correct.
|
||||||
|
"""
|
||||||
|
|
||||||
|
duration = 33
|
||||||
|
temp_file = tmp_path / "temp.avif"
|
||||||
|
with self.star_frames() as frames:
|
||||||
|
frames[0].save(
|
||||||
|
temp_file,
|
||||||
|
save_all=True,
|
||||||
|
append_images=(frames[1:] + [frames[0]]),
|
||||||
|
duration=duration,
|
||||||
|
)
|
||||||
|
|
||||||
|
with Image.open(temp_file) as im:
|
||||||
|
assert im.n_frames == 5
|
||||||
|
assert im.is_animated
|
||||||
|
|
||||||
|
# Traverse frames in reverse, checking timestamps and durations
|
||||||
|
timestamp = duration * (im.n_frames - 1)
|
||||||
|
for frame in reversed(range(im.n_frames)):
|
||||||
|
im.seek(frame)
|
||||||
|
im.load()
|
||||||
|
assert im.info["duration"] == duration
|
||||||
|
assert im.info["timestamp"] == timestamp
|
||||||
|
timestamp -= duration
|
||||||
|
|
||||||
|
def test_seek_errors(self) -> None:
|
||||||
|
with Image.open("Tests/images/avif/star.avifs") as im:
|
||||||
|
with pytest.raises(EOFError):
|
||||||
|
im.seek(-1)
|
||||||
|
|
||||||
|
with pytest.raises(EOFError):
|
||||||
|
im.seek(42)
|
||||||
|
|
||||||
|
|
||||||
|
MAX_THREADS = os.cpu_count() or 1
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("avif")
|
||||||
|
class TestAvifLeaks(PillowLeakTestCase):
|
||||||
|
mem_limit = MAX_THREADS * 3 * 1024
|
||||||
|
iterations = 100
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
is_docker_qemu(), reason="Skipping on cross-architecture containers"
|
||||||
|
)
|
||||||
|
def test_leak_load(self) -> None:
|
||||||
|
with open(TEST_AVIF_FILE, "rb") as f:
|
||||||
|
im_data = f.read()
|
||||||
|
|
||||||
|
def core() -> None:
|
||||||
|
with Image.open(BytesIO(im_data)) as im:
|
||||||
|
im.load()
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
self._test_leak(core)
|
|
@ -69,12 +69,14 @@ def test_tell() -> None:
|
||||||
|
|
||||||
def test_n_frames() -> None:
|
def test_n_frames() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, DcxImagePlugin.DcxImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror() -> None:
|
def test_eoferror() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, DcxImagePlugin.DcxImageFile)
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
# Test seeking past the last frame
|
# Test seeking past the last frame
|
||||||
|
|
|
@ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = (
|
||||||
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
|
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
|
||||||
expected_size = tuple(s * scale for s in size)
|
expected_size = tuple(s * scale for s in size)
|
||||||
with Image.open(filename) as image:
|
with Image.open(filename) as image:
|
||||||
|
assert isinstance(image, EpsImagePlugin.EpsImageFile)
|
||||||
|
|
||||||
image.load(scale=scale)
|
image.load(scale=scale)
|
||||||
assert image.mode == "RGB"
|
assert image.mode == "RGB"
|
||||||
assert image.size == expected_size
|
assert image.size == expected_size
|
||||||
|
@ -227,6 +229,8 @@ def test_showpage() -> None:
|
||||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||||
def test_transparency() -> None:
|
def test_transparency() -> None:
|
||||||
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
|
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
|
||||||
|
assert isinstance(plot_image, EpsImagePlugin.EpsImageFile)
|
||||||
|
|
||||||
plot_image.load(transparency=True)
|
plot_image.load(transparency=True)
|
||||||
assert plot_image.mode == "RGBA"
|
assert plot_image.mode == "RGBA"
|
||||||
|
|
||||||
|
@ -308,6 +312,7 @@ def test_render_scale2() -> None:
|
||||||
|
|
||||||
# Zero bounding box
|
# Zero bounding box
|
||||||
with Image.open(FILE1) as image1_scale2:
|
with Image.open(FILE1) as image1_scale2:
|
||||||
|
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
|
||||||
image1_scale2.load(scale=2)
|
image1_scale2.load(scale=2)
|
||||||
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
|
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
|
||||||
image1_scale2_compare = image1_scale2_compare.convert("RGB")
|
image1_scale2_compare = image1_scale2_compare.convert("RGB")
|
||||||
|
@ -316,6 +321,7 @@ def test_render_scale2() -> None:
|
||||||
|
|
||||||
# Non-zero bounding box
|
# Non-zero bounding box
|
||||||
with Image.open(FILE2) as image2_scale2:
|
with Image.open(FILE2) as image2_scale2:
|
||||||
|
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
|
||||||
image2_scale2.load(scale=2)
|
image2_scale2.load(scale=2)
|
||||||
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
|
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
|
||||||
image2_scale2_compare = image2_scale2_compare.convert("RGB")
|
image2_scale2_compare = image2_scale2_compare.convert("RGB")
|
||||||
|
|
|
@ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
|
||||||
|
|
||||||
def test_sanity() -> None:
|
def test_sanity() -> None:
|
||||||
with Image.open(static_test_file) as im:
|
with Image.open(static_test_file) as im:
|
||||||
|
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
assert im.size == (128, 128)
|
assert im.size == (128, 128)
|
||||||
|
@ -29,6 +31,8 @@ def test_sanity() -> None:
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
|
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||||
|
|
||||||
assert im.mode == "P"
|
assert im.mode == "P"
|
||||||
assert im.size == (320, 200)
|
assert im.size == (320, 200)
|
||||||
assert im.format == "FLI"
|
assert im.format == "FLI"
|
||||||
|
@ -112,16 +116,19 @@ def test_palette_chunk_second() -> None:
|
||||||
|
|
||||||
def test_n_frames() -> None:
|
def test_n_frames() -> None:
|
||||||
with Image.open(static_test_file) as im:
|
with Image.open(static_test_file) as im:
|
||||||
|
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
|
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||||
assert im.n_frames == 384
|
assert im.n_frames == 384
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror() -> None:
|
def test_eoferror() -> None:
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
|
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
# Test seeking past the last frame
|
# Test seeking past the last frame
|
||||||
|
@ -166,6 +173,7 @@ def test_seek_tell() -> None:
|
||||||
|
|
||||||
def test_seek() -> None:
|
def test_seek() -> None:
|
||||||
with Image.open(animated_test_file) as im:
|
with Image.open(animated_test_file) as im:
|
||||||
|
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||||
im.seek(50)
|
im.seek(50)
|
||||||
|
|
||||||
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
|
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
|
||||||
|
|
|
@ -22,10 +22,11 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
def test_close() -> None:
|
def test_close() -> None:
|
||||||
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||||
pass
|
assert isinstance(im, FpxImagePlugin.FpxImageFile)
|
||||||
assert im.ole.fp.closed
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
im = Image.open("Tests/images/input_bw_one_band.fpx")
|
im = Image.open("Tests/images/input_bw_one_band.fpx")
|
||||||
|
assert isinstance(im, FpxImagePlugin.FpxImageFile)
|
||||||
im.close()
|
im.close()
|
||||||
assert im.ole.fp.closed
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
|
|
@ -402,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def test_seek() -> None:
|
def test_seek() -> None:
|
||||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||||
|
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
@ -446,10 +447,12 @@ def test_seek_rewind() -> None:
|
||||||
def test_n_frames(path: str, n_frames: int) -> None:
|
def test_n_frames(path: str, n_frames: int) -> None:
|
||||||
# Test is_animated before n_frames
|
# Test is_animated before n_frames
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
assert im.is_animated == (n_frames != 1)
|
assert im.is_animated == (n_frames != 1)
|
||||||
|
|
||||||
# Test is_animated after n_frames
|
# Test is_animated after n_frames
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
assert im.n_frames == n_frames
|
assert im.n_frames == n_frames
|
||||||
assert im.is_animated == (n_frames != 1)
|
assert im.is_animated == (n_frames != 1)
|
||||||
|
|
||||||
|
@ -459,6 +462,7 @@ def test_no_change() -> None:
|
||||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
expected = im.copy()
|
expected = im.copy()
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
assert im.n_frames == 5
|
assert im.n_frames == 5
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
@ -466,17 +470,20 @@ def test_no_change() -> None:
|
||||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||||
im.seek(3)
|
im.seek(3)
|
||||||
expected = im.copy()
|
expected = im.copy()
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
|
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
|
||||||
expected = Image.new("P", (1, 1))
|
expected = Image.new("P", (1, 1))
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
assert_image_equal(im, expected)
|
assert_image_equal(im, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror() -> None:
|
def test_eoferror() -> None:
|
||||||
with Image.open(TEST_GIF) as im:
|
with Image.open(TEST_GIF) as im:
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
# Test seeking past the last frame
|
# Test seeking past the last frame
|
||||||
|
@ -495,6 +502,7 @@ def test_first_frame_transparency() -> None:
|
||||||
|
|
||||||
def test_dispose_none() -> None:
|
def test_dispose_none() -> None:
|
||||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||||
|
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
img.seek(img.tell() + 1)
|
img.seek(img.tell() + 1)
|
||||||
|
@ -518,6 +526,7 @@ def test_dispose_none_load_end() -> None:
|
||||||
|
|
||||||
def test_dispose_background() -> None:
|
def test_dispose_background() -> None:
|
||||||
with Image.open("Tests/images/dispose_bgnd.gif") as img:
|
with Image.open("Tests/images/dispose_bgnd.gif") as img:
|
||||||
|
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
img.seek(img.tell() + 1)
|
img.seek(img.tell() + 1)
|
||||||
|
@ -571,6 +580,7 @@ def test_transparent_dispose(
|
||||||
|
|
||||||
def test_dispose_previous() -> None:
|
def test_dispose_previous() -> None:
|
||||||
with Image.open("Tests/images/dispose_prev.gif") as img:
|
with Image.open("Tests/images/dispose_prev.gif") as img:
|
||||||
|
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
img.seek(img.tell() + 1)
|
img.seek(img.tell() + 1)
|
||||||
|
@ -608,6 +618,7 @@ def test_save_dispose(tmp_path: Path) -> None:
|
||||||
for method in range(4):
|
for method in range(4):
|
||||||
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
|
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
|
||||||
with Image.open(out) as img:
|
with Image.open(out) as img:
|
||||||
|
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
img.seek(img.tell() + 1)
|
img.seek(img.tell() + 1)
|
||||||
assert img.disposal_method == method
|
assert img.disposal_method == method
|
||||||
|
@ -621,6 +632,7 @@ def test_save_dispose(tmp_path: Path) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(out) as img:
|
with Image.open(out) as img:
|
||||||
|
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
img.seek(img.tell() + 1)
|
img.seek(img.tell() + 1)
|
||||||
assert img.disposal_method == i + 1
|
assert img.disposal_method == i + 1
|
||||||
|
@ -743,6 +755,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None:
|
||||||
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
|
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
|
||||||
|
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
assert im.n_frames == 3
|
assert im.n_frames == 3
|
||||||
|
|
||||||
|
|
||||||
|
@ -924,6 +937,8 @@ def test_identical_frames(tmp_path: Path) -> None:
|
||||||
out, save_all=True, append_images=im_list[1:], duration=duration_list
|
out, save_all=True, append_images=im_list[1:], duration=duration_list
|
||||||
)
|
)
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
|
|
||||||
# Assert that the first three frames were combined
|
# Assert that the first three frames were combined
|
||||||
assert reread.n_frames == 2
|
assert reread.n_frames == 2
|
||||||
|
|
||||||
|
@ -953,6 +968,8 @@ def test_identical_frames_to_single_frame(
|
||||||
|
|
||||||
im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
|
im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
|
|
||||||
# Assert that all frames were combined
|
# Assert that all frames were combined
|
||||||
assert reread.n_frames == 1
|
assert reread.n_frames == 1
|
||||||
|
|
||||||
|
@ -1139,12 +1156,14 @@ def test_append_images(tmp_path: Path) -> None:
|
||||||
im.copy().save(out, save_all=True, append_images=ims)
|
im.copy().save(out, save_all=True, append_images=ims)
|
||||||
|
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Test append_images without save_all
|
# Test append_images without save_all
|
||||||
im.copy().save(out, append_images=ims)
|
im.copy().save(out, append_images=ims)
|
||||||
|
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Tests appending using a generator
|
# Tests appending using a generator
|
||||||
|
@ -1154,6 +1173,7 @@ def test_append_images(tmp_path: Path) -> None:
|
||||||
im.save(out, save_all=True, append_images=im_generator(ims))
|
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||||
|
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Tests appending single and multiple frame images
|
# Tests appending single and multiple frame images
|
||||||
|
@ -1162,6 +1182,7 @@ def test_append_images(tmp_path: Path) -> None:
|
||||||
im.save(out, save_all=True, append_images=[im2])
|
im.save(out, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
assert reread.n_frames == 10
|
assert reread.n_frames == 10
|
||||||
|
|
||||||
|
|
||||||
|
@ -1262,6 +1283,7 @@ def test_bbox(tmp_path: Path) -> None:
|
||||||
im.save(out, save_all=True, append_images=ims)
|
im.save(out, save_all=True, append_images=ims)
|
||||||
|
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
assert reread.n_frames == 2
|
assert reread.n_frames == 2
|
||||||
|
|
||||||
|
|
||||||
|
@ -1274,6 +1296,7 @@ def test_bbox_alpha(tmp_path: Path) -> None:
|
||||||
im.save(out, save_all=True, append_images=[im2])
|
im.save(out, save_all=True, append_images=[im2])
|
||||||
|
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||||
assert reread.n_frames == 2
|
assert reread.n_frames == 2
|
||||||
|
|
||||||
|
|
||||||
|
@ -1425,6 +1448,7 @@ def test_extents(
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
|
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
|
||||||
with Image.open("Tests/images/" + test_file) as im:
|
with Image.open("Tests/images/" + test_file) as im:
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
assert im.size == (100, 100)
|
assert im.size == (100, 100)
|
||||||
|
|
||||||
# Check that n_frames does not change the size
|
# Check that n_frames does not change the size
|
||||||
|
@ -1472,4 +1496,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
|
||||||
im1.save(out, save_all=True, append_images=[im2], **params)
|
im1.save(out, save_all=True, append_images=[im2], **params)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, GifImagePlugin.GifImageFile)
|
||||||
assert reloaded.n_frames == 2
|
assert reloaded.n_frames == 2
|
||||||
|
|
|
@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None:
|
||||||
assert_image_similar_tofile(im, temp_file, 1)
|
assert_image_similar_tofile(im, temp_file, 1)
|
||||||
|
|
||||||
with Image.open(temp_file) as reread:
|
with Image.open(temp_file) as reread:
|
||||||
|
assert isinstance(reread, IcnsImagePlugin.IcnsImageFile)
|
||||||
reread.size = (16, 16)
|
reread.size = (16, 16)
|
||||||
reread.load(2)
|
reread.load(2)
|
||||||
assert_image_equal(reread, provided_im)
|
assert_image_equal(reread, provided_im)
|
||||||
|
@ -90,6 +91,7 @@ def test_sizes() -> None:
|
||||||
# Check that we can load all of the sizes, and that the final pixel
|
# Check that we can load all of the sizes, and that the final pixel
|
||||||
# dimensions are as expected
|
# dimensions are as expected
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, IcnsImagePlugin.IcnsImageFile)
|
||||||
for w, h, r in im.info["sizes"]:
|
for w, h, r in im.info["sizes"]:
|
||||||
wr = w * r
|
wr = w * r
|
||||||
hr = h * r
|
hr = h * r
|
||||||
|
@ -118,6 +120,7 @@ def test_older_icon() -> None:
|
||||||
wr = w * r
|
wr = w * r
|
||||||
hr = h * r
|
hr = h * r
|
||||||
with Image.open("Tests/images/pillow2.icns") as im2:
|
with Image.open("Tests/images/pillow2.icns") as im2:
|
||||||
|
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
|
||||||
im2.size = (w, h)
|
im2.size = (w, h)
|
||||||
im2.load(r)
|
im2.load(r)
|
||||||
assert im2.mode == "RGBA"
|
assert im2.mode == "RGBA"
|
||||||
|
@ -135,6 +138,7 @@ def test_jp2_icon() -> None:
|
||||||
wr = w * r
|
wr = w * r
|
||||||
hr = h * r
|
hr = h * r
|
||||||
with Image.open("Tests/images/pillow3.icns") as im2:
|
with Image.open("Tests/images/pillow3.icns") as im2:
|
||||||
|
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
|
||||||
im2.size = (w, h)
|
im2.size = (w, h)
|
||||||
im2.load(r)
|
im2.load(r)
|
||||||
assert im2.mode == "RGBA"
|
assert im2.mode == "RGBA"
|
||||||
|
|
|
@ -77,6 +77,7 @@ def test_save_to_bytes() -> None:
|
||||||
# The other one
|
# The other one
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
with Image.open(output) as reloaded:
|
with Image.open(output) as reloaded:
|
||||||
|
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||||
reloaded.size = (32, 32)
|
reloaded.size = (32, 32)
|
||||||
|
|
||||||
assert im.mode == reloaded.mode
|
assert im.mode == reloaded.mode
|
||||||
|
@ -94,6 +95,7 @@ def test_getpixel(tmp_path: Path) -> None:
|
||||||
im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
|
im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
|
||||||
|
|
||||||
with Image.open(temp_file) as reloaded:
|
with Image.open(temp_file) as reloaded:
|
||||||
|
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||||
reloaded.load()
|
reloaded.load()
|
||||||
reloaded.size = (32, 32)
|
reloaded.size = (32, 32)
|
||||||
|
|
||||||
|
@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
|
||||||
# The other one
|
# The other one
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
with Image.open(output) as reloaded:
|
with Image.open(output) as reloaded:
|
||||||
|
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||||
reloaded.size = (32, 32)
|
reloaded.size = (32, 32)
|
||||||
|
|
||||||
assert "RGBA" == reloaded.mode
|
assert "RGBA" == reloaded.mode
|
||||||
|
@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
|
||||||
|
|
||||||
def test_incorrect_size() -> None:
|
def test_incorrect_size() -> None:
|
||||||
with Image.open(TEST_ICO_FILE) as im:
|
with Image.open(TEST_ICO_FILE) as im:
|
||||||
|
assert isinstance(im, IcoImagePlugin.IcoImageFile)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.size = (1, 1)
|
im.size = (1, 1)
|
||||||
|
|
||||||
|
@ -219,6 +223,7 @@ def test_save_append_images(tmp_path: Path) -> None:
|
||||||
im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im])
|
im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im])
|
||||||
|
|
||||||
with Image.open(outfile) as reread:
|
with Image.open(outfile) as reread:
|
||||||
|
assert isinstance(reread, IcoImagePlugin.IcoImageFile)
|
||||||
assert_image_equal(reread, hopper("RGBA"))
|
assert_image_equal(reread, hopper("RGBA"))
|
||||||
|
|
||||||
reread.size = (32, 32)
|
reread.size = (32, 32)
|
||||||
|
|
|
@ -68,12 +68,14 @@ def test_tell() -> None:
|
||||||
|
|
||||||
def test_n_frames() -> None:
|
def test_n_frames() -> None:
|
||||||
with Image.open(TEST_IM) as im:
|
with Image.open(TEST_IM) as im:
|
||||||
|
assert isinstance(im, ImImagePlugin.ImImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror() -> None:
|
def test_eoferror() -> None:
|
||||||
with Image.open(TEST_IM) as im:
|
with Image.open(TEST_IM) as im:
|
||||||
|
assert isinstance(im, ImImagePlugin.ImImageFile)
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
# Test seeking past the last frame
|
# Test seeking past the last frame
|
||||||
|
|
|
@ -91,6 +91,7 @@ class TestFileJpeg:
|
||||||
def test_app(self) -> None:
|
def test_app(self) -> None:
|
||||||
# Test APP/COM reader (@PIL135)
|
# Test APP/COM reader (@PIL135)
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")
|
assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")
|
||||||
assert im.applist[1] == (
|
assert im.applist[1] == (
|
||||||
"COM",
|
"COM",
|
||||||
|
@ -316,6 +317,8 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_exif_typeerror(self) -> None:
|
def test_exif_typeerror(self) -> None:
|
||||||
with Image.open("Tests/images/exif_typeerror.jpg") as im:
|
with Image.open("Tests/images/exif_typeerror.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
|
|
||||||
# Should not raise a TypeError
|
# Should not raise a TypeError
|
||||||
im._getexif()
|
im._getexif()
|
||||||
|
|
||||||
|
@ -500,6 +503,7 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_mp(self) -> None:
|
def test_mp(self) -> None:
|
||||||
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
|
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
assert im._getmp() is None
|
assert im._getmp() is None
|
||||||
|
|
||||||
def test_quality_keep(self, tmp_path: Path) -> None:
|
def test_quality_keep(self, tmp_path: Path) -> None:
|
||||||
|
@ -558,12 +562,14 @@ class TestFileJpeg:
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
im.save(b, "JPEG", qtables=[[n] * 64] * n)
|
im.save(b, "JPEG", qtables=[[n] * 64] * n)
|
||||||
with Image.open(b) as im:
|
with Image.open(b) as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
assert len(im.quantization) == n
|
assert len(im.quantization) == n
|
||||||
reloaded = self.roundtrip(im, qtables="keep")
|
reloaded = self.roundtrip(im, qtables="keep")
|
||||||
assert im.quantization == reloaded.quantization
|
assert im.quantization == reloaded.quantization
|
||||||
assert max(reloaded.quantization[0]) <= 255
|
assert max(reloaded.quantization[0]) <= 255
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
qtables = im.quantization
|
qtables = im.quantization
|
||||||
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
|
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
|
||||||
assert im.quantization == reloaded.quantization
|
assert im.quantization == reloaded.quantization
|
||||||
|
@ -663,6 +669,7 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_load_16bit_qtables(self) -> None:
|
def test_load_16bit_qtables(self) -> None:
|
||||||
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
|
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
assert len(im.quantization) == 2
|
assert len(im.quantization) == 2
|
||||||
assert len(im.quantization[0]) == 64
|
assert len(im.quantization[0]) == 64
|
||||||
assert max(im.quantization[0]) > 255
|
assert max(im.quantization[0]) > 255
|
||||||
|
@ -705,6 +712,7 @@ class TestFileJpeg:
|
||||||
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
||||||
def test_load_djpeg(self) -> None:
|
def test_load_djpeg(self) -> None:
|
||||||
with Image.open(TEST_FILE) as img:
|
with Image.open(TEST_FILE) as img:
|
||||||
|
assert isinstance(img, JpegImagePlugin.JpegImageFile)
|
||||||
img.load_djpeg()
|
img.load_djpeg()
|
||||||
assert_image_similar_tofile(img, TEST_FILE, 5)
|
assert_image_similar_tofile(img, TEST_FILE, 5)
|
||||||
|
|
||||||
|
@ -909,6 +917,7 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_photoshop_malformed_and_multiple(self) -> None:
|
def test_photoshop_malformed_and_multiple(self) -> None:
|
||||||
with Image.open("Tests/images/app13-multiple.jpg") as im:
|
with Image.open("Tests/images/app13-multiple.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
assert "photoshop" in im.info
|
assert "photoshop" in im.info
|
||||||
assert 24 == len(im.info["photoshop"])
|
assert 24 == len(im.info["photoshop"])
|
||||||
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
|
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
|
||||||
|
@ -1084,6 +1093,7 @@ class TestFileJpeg:
|
||||||
|
|
||||||
def test_deprecation(self) -> None:
|
def test_deprecation(self) -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
assert im.huffman_ac == {}
|
assert im.huffman_ac == {}
|
||||||
with pytest.warns(DeprecationWarning):
|
with pytest.warns(DeprecationWarning):
|
||||||
|
|
|
@ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None:
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
|
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
|
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
|
||||||
im.layers = 1
|
im.layers = 1
|
||||||
im.load()
|
im.load()
|
||||||
assert_image_similar(im, card, 13)
|
assert_image_similar(im, card, 13)
|
||||||
|
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
with Image.open(out) as im:
|
with Image.open(out) as im:
|
||||||
|
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
|
||||||
im.layers = 3
|
im.layers = 3
|
||||||
im.load()
|
im.load()
|
||||||
assert_image_similar(im, card, 0.4)
|
assert_image_similar(im, card, 0.4)
|
||||||
|
|
|
@ -36,6 +36,7 @@ class LibTiffTestCase:
|
||||||
im.load()
|
im.load()
|
||||||
im.getdata()
|
im.getdata()
|
||||||
|
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im._compression == "group4"
|
assert im._compression == "group4"
|
||||||
|
|
||||||
# can we write it back out, in a different form.
|
# can we write it back out, in a different form.
|
||||||
|
@ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
"""Test metadata writing through libtiff"""
|
"""Test metadata writing through libtiff"""
|
||||||
f = tmp_path / "temp.tiff"
|
f = tmp_path / "temp.tiff"
|
||||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||||
|
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||||
img.save(f, tiffinfo=img.tag)
|
img.save(f, tiffinfo=img.tag)
|
||||||
|
|
||||||
if legacy_api:
|
if legacy_api:
|
||||||
|
@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
|
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||||
if legacy_api:
|
if legacy_api:
|
||||||
reloaded = loaded.tag.named()
|
reloaded = loaded.tag.named()
|
||||||
else:
|
else:
|
||||||
|
@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
# Exclude ones that have special meaning
|
# Exclude ones that have special meaning
|
||||||
# that we're already testing them
|
# that we're already testing them
|
||||||
with Image.open("Tests/images/hopper_g4.tif") as im:
|
with Image.open("Tests/images/hopper_g4.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
for tag in im.tag_v2:
|
for tag in im.tag_v2:
|
||||||
try:
|
try:
|
||||||
del core_items[tag]
|
del core_items[tag]
|
||||||
|
@ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im.save(out, tiffinfo=tiffinfo)
|
im.save(out, tiffinfo=tiffinfo)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
for tag, value in tiffinfo.items():
|
for tag, value in tiffinfo.items():
|
||||||
reloaded_value = reloaded.tag_v2[tag]
|
reloaded_value = reloaded.tag_v2[tag]
|
||||||
if (
|
if (
|
||||||
|
@ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test_osubfiletype(self, tmp_path: Path) -> None:
|
def test_osubfiletype(self, tmp_path: Path) -> None:
|
||||||
outfile = tmp_path / "temp.tif"
|
outfile = tmp_path / "temp.tif"
|
||||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.tag_v2[OSUBFILETYPE] = 1
|
im.tag_v2[OSUBFILETYPE] = 1
|
||||||
im.save(outfile)
|
im.save(outfile)
|
||||||
|
|
||||||
def test_subifd(self, tmp_path: Path) -> None:
|
def test_subifd(self, tmp_path: Path) -> None:
|
||||||
outfile = tmp_path / "temp.tif"
|
outfile = tmp_path / "temp.tif"
|
||||||
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
with Image.open("Tests/images/g4_orientation_6.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.tag_v2[SUBIFD] = 10000
|
im.tag_v2[SUBIFD] = 10000
|
||||||
|
|
||||||
# Should not segfault
|
# Should not segfault
|
||||||
|
@ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
|
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
if 700 in reloaded.tag_v2:
|
if 700 in reloaded.tag_v2:
|
||||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||||
|
|
||||||
|
@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
"""Tests String data in info directory"""
|
"""Tests String data in info directory"""
|
||||||
test_file = "Tests/images/hopper_g4_500.tif"
|
test_file = "Tests/images/hopper_g4_500.tif"
|
||||||
with Image.open(test_file) as orig:
|
with Image.open(test_file) as orig:
|
||||||
|
assert isinstance(orig, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
out = tmp_path / "temp.tif"
|
out = tmp_path / "temp.tif"
|
||||||
|
|
||||||
orig.tag[269] = "temp.tif"
|
orig.tag[269] = "temp.tif"
|
||||||
orig.save(out)
|
orig.save(out)
|
||||||
|
|
||||||
with Image.open(out) as reread:
|
with Image.open(out) as reread:
|
||||||
|
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||||
assert "temp.tif" == reread.tag_v2[269]
|
assert "temp.tif" == reread.tag_v2[269]
|
||||||
assert "temp.tif" == reread.tag[269][0]
|
assert "temp.tif" == reread.tag[269][0]
|
||||||
|
|
||||||
|
@ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
# colormap/palette tag
|
# colormap/palette tag
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert len(reloaded.tag_v2[320]) == 768
|
assert len(reloaded.tag_v2[320]) == 768
|
||||||
|
|
||||||
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
|
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
|
||||||
|
@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
||||||
|
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
assert im.size == (10, 10)
|
assert im.size == (10, 10)
|
||||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
||||||
|
@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
# issue #862
|
# issue #862
|
||||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
frames = im.n_frames
|
frames = im.n_frames
|
||||||
assert frames == 3
|
assert frames == 3
|
||||||
for _ in range(frames):
|
for _ in range(frames):
|
||||||
|
@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||||
with Image.open("Tests/images/hopper.tif") as im:
|
with Image.open("Tests/images/hopper.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert not im.tag.next
|
assert not im.tag.next
|
||||||
im.load()
|
im.load()
|
||||||
assert not im.tag.next
|
assert not im.tag.next
|
||||||
|
@ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
im.save(outfile, compression="jpeg")
|
im.save(outfile, compression="jpeg")
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[530] == (1, 1)
|
assert reloaded.tag_v2[530] == (1, 1)
|
||||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||||
|
|
||||||
def test_exif_ifd(self) -> None:
|
def test_exif_ifd(self) -> None:
|
||||||
out = io.BytesIO()
|
out = io.BytesIO()
|
||||||
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2[34665] == 125456
|
assert im.tag_v2[34665] == 125456
|
||||||
im.save(out, "TIFF")
|
im.save(out, "TIFF")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert 34665 not in reloaded.tag_v2
|
assert 34665 not in reloaded.tag_v2
|
||||||
|
|
||||||
im.save(out, "TIFF", tiffinfo={34665: 125456})
|
im.save(out, "TIFF", tiffinfo={34665: 125456})
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
if Image.core.libtiff_support_custom_tags:
|
if Image.core.libtiff_support_custom_tags:
|
||||||
assert reloaded.tag_v2[34665] == 125456
|
assert reloaded.tag_v2[34665] == 125456
|
||||||
|
|
||||||
|
@ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
|
|
||||||
def test_multipage_compression(self) -> None:
|
def test_multipage_compression(self) -> None:
|
||||||
with Image.open("Tests/images/compression.tif") as im:
|
with Image.open("Tests/images/compression.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
assert im._compression == "tiff_ccitt"
|
assert im._compression == "tiff_ccitt"
|
||||||
assert im.size == (10, 10)
|
assert im.size == (10, 10)
|
||||||
|
@ -1090,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||||
for i in range(2, 9):
|
for i in range(2, 9):
|
||||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert 274 in im.tag_v2
|
assert 274 in im.tag_v2
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
|
|
|
@ -30,11 +30,13 @@ def test_sanity() -> None:
|
||||||
|
|
||||||
def test_n_frames() -> None:
|
def test_n_frames() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
|
||||||
|
|
||||||
def test_is_animated() -> None:
|
def test_is_animated() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,10 +57,11 @@ def test_seek() -> None:
|
||||||
|
|
||||||
def test_close() -> None:
|
def test_close() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
pass
|
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||||
assert im.ole.fp.closed
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
im = Image.open(TEST_FILE)
|
im = Image.open(TEST_FILE)
|
||||||
|
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||||
im.close()
|
im.close()
|
||||||
assert im.ole.fp.closed
|
assert im.ole.fp.closed
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFile, MpoImagePlugin
|
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -80,6 +80,7 @@ def test_context_manager() -> None:
|
||||||
def test_app(test_file: str) -> None:
|
def test_app(test_file: str) -> None:
|
||||||
# Test APP/COM reader (@PIL135)
|
# Test APP/COM reader (@PIL135)
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||||
assert im.applist[0][0] == "APP1"
|
assert im.applist[0][0] == "APP1"
|
||||||
assert im.applist[1][0] == "APP2"
|
assert im.applist[1][0] == "APP2"
|
||||||
assert im.applist[1][1].startswith(
|
assert im.applist[1][1].startswith(
|
||||||
|
@ -220,12 +221,14 @@ def test_seek(test_file: str) -> None:
|
||||||
|
|
||||||
def test_n_frames() -> None:
|
def test_n_frames() -> None:
|
||||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||||
|
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror() -> None:
|
def test_eoferror() -> None:
|
||||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||||
|
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
# Test seeking past the last frame
|
# Test seeking past the last frame
|
||||||
|
@ -239,6 +242,8 @@ def test_eoferror() -> None:
|
||||||
|
|
||||||
def test_adopt_jpeg() -> None:
|
def test_adopt_jpeg() -> None:
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
MpoImagePlugin.MpoImageFile.adopt(im)
|
MpoImagePlugin.MpoImageFile.adopt(im)
|
||||||
|
|
||||||
|
|
|
@ -576,6 +576,7 @@ class TestFilePng:
|
||||||
|
|
||||||
def test_read_private_chunks(self) -> None:
|
def test_read_private_chunks(self) -> None:
|
||||||
with Image.open("Tests/images/exif.png") as im:
|
with Image.open("Tests/images/exif.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.private_chunks == [(b"orNT", b"\x01")]
|
assert im.private_chunks == [(b"orNT", b"\x01")]
|
||||||
|
|
||||||
def test_roundtrip_private_chunk(self) -> None:
|
def test_roundtrip_private_chunk(self) -> None:
|
||||||
|
@ -598,6 +599,7 @@ class TestFilePng:
|
||||||
|
|
||||||
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
with Image.open("Tests/images/hopper.png") as im:
|
with Image.open("Tests/images/hopper.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert "comment" in im.text
|
assert "comment" in im.text
|
||||||
for k, v in {
|
for k, v in {
|
||||||
"date:create": "2014-09-04T09:37:08+03:00",
|
"date:create": "2014-09-04T09:37:08+03:00",
|
||||||
|
@ -607,15 +609,19 @@ class TestFilePng:
|
||||||
|
|
||||||
# Raises a SyntaxError in load_end
|
# Raises a SyntaxError in load_end
|
||||||
with Image.open("Tests/images/broken_data_stream.png") as im:
|
with Image.open("Tests/images/broken_data_stream.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
assert isinstance(im.text, dict)
|
assert isinstance(im.text, dict)
|
||||||
|
|
||||||
# Raises an EOFError in load_end
|
# Raises an EOFError in load_end
|
||||||
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
||||||
|
|
||||||
# Raises a UnicodeDecodeError in load_end
|
# Raises a UnicodeDecodeError in load_end
|
||||||
with Image.open("Tests/images/truncated_image.png") as im:
|
with Image.open("Tests/images/truncated_image.png") as im:
|
||||||
|
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||||
|
|
||||||
# The file is truncated
|
# The file is truncated
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
im.text
|
im.text
|
||||||
|
@ -726,6 +732,7 @@ class TestFilePng:
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
|
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
|
||||||
assert reloaded._getexif() is None
|
assert reloaded._getexif() is None
|
||||||
|
|
||||||
# Test passing in exif
|
# Test passing in exif
|
||||||
|
|
|
@ -59,17 +59,21 @@ def test_invalid_file() -> None:
|
||||||
|
|
||||||
def test_n_frames() -> None:
|
def test_n_frames() -> None:
|
||||||
with Image.open("Tests/images/hopper_merged.psd") as im:
|
with Image.open("Tests/images/hopper_merged.psd") as im:
|
||||||
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
|
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
||||||
def test_eoferror() -> None:
|
def test_eoferror() -> None:
|
||||||
with Image.open(test_file) as im:
|
with Image.open(test_file) as im:
|
||||||
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
|
|
||||||
# PSD seek index starts at 1 rather than 0
|
# PSD seek index starts at 1 rather than 0
|
||||||
n_frames = im.n_frames + 1
|
n_frames = im.n_frames + 1
|
||||||
|
|
||||||
|
@ -119,11 +123,13 @@ def test_rgba() -> None:
|
||||||
|
|
||||||
def test_negative_top_left_layer() -> None:
|
def test_negative_top_left_layer() -> None:
|
||||||
with Image.open("Tests/images/negative_top_left_layer.psd") as im:
|
with Image.open("Tests/images/negative_top_left_layer.psd") as im:
|
||||||
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
assert im.layers[0][2] == (-50, -50, 50, 50)
|
assert im.layers[0][2] == (-50, -50, 50, 50)
|
||||||
|
|
||||||
|
|
||||||
def test_layer_skip() -> None:
|
def test_layer_skip() -> None:
|
||||||
with Image.open("Tests/images/five_channels.psd") as im:
|
with Image.open("Tests/images/five_channels.psd") as im:
|
||||||
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
||||||
def test_layer_crashes(test_file: str) -> None:
|
def test_layer_crashes(test_file: str) -> None:
|
||||||
with open(test_file, "rb") as f:
|
with open(test_file, "rb") as f:
|
||||||
with Image.open(f) as im:
|
with Image.open(f) as im:
|
||||||
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
im.layers
|
im.layers
|
||||||
|
|
|
@ -96,6 +96,7 @@ def test_tell() -> None:
|
||||||
|
|
||||||
def test_n_frames() -> None:
|
def test_n_frames() -> None:
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, SpiderImagePlugin.SpiderImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,13 @@ from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError
|
from PIL import (
|
||||||
|
Image,
|
||||||
|
ImageFile,
|
||||||
|
JpegImagePlugin,
|
||||||
|
TiffImagePlugin,
|
||||||
|
UnidentifiedImageError,
|
||||||
|
)
|
||||||
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
|
@ -113,6 +119,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
|
||||||
outfile = tmp_path / "temp.tif"
|
outfile = tmp_path / "temp.tif"
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||||
|
|
||||||
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||||
|
@ -121,11 +128,13 @@ class TestFileTiff:
|
||||||
im.save(outfile, big_tiff=True)
|
im.save(outfile, big_tiff=True)
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2._bigtiff is True
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
||||||
|
|
||||||
with Image.open(outfile) as reloaded:
|
with Image.open(outfile) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2._bigtiff is True
|
assert reloaded.tag_v2._bigtiff is True
|
||||||
|
|
||||||
def test_seek_too_large(self) -> None:
|
def test_seek_too_large(self) -> None:
|
||||||
|
@ -140,6 +149,8 @@ class TestFileTiff:
|
||||||
def test_xyres_tiff(self) -> None:
|
def test_xyres_tiff(self) -> None:
|
||||||
filename = "Tests/images/pil168.tif"
|
filename = "Tests/images/pil168.tif"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# legacy api
|
# legacy api
|
||||||
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
|
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
|
||||||
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
|
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
|
||||||
|
@ -153,6 +164,8 @@ class TestFileTiff:
|
||||||
def test_xyres_fallback_tiff(self) -> None:
|
def test_xyres_fallback_tiff(self) -> None:
|
||||||
filename = "Tests/images/compression.tif"
|
filename = "Tests/images/compression.tif"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# v2 api
|
# v2 api
|
||||||
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
|
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||||
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
|
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||||
|
@ -167,6 +180,8 @@ class TestFileTiff:
|
||||||
def test_int_resolution(self) -> None:
|
def test_int_resolution(self) -> None:
|
||||||
filename = "Tests/images/pil168.tif"
|
filename = "Tests/images/pil168.tif"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# Try to read a file where X,Y_RESOLUTION are ints
|
# Try to read a file where X,Y_RESOLUTION are ints
|
||||||
im.tag_v2[X_RESOLUTION] = 71
|
im.tag_v2[X_RESOLUTION] = 71
|
||||||
im.tag_v2[Y_RESOLUTION] = 71
|
im.tag_v2[Y_RESOLUTION] = 71
|
||||||
|
@ -181,6 +196,7 @@ class TestFileTiff:
|
||||||
with Image.open(
|
with Image.open(
|
||||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||||
) as im:
|
) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
||||||
assert im.info["dpi"] == (dpi, dpi)
|
assert im.info["dpi"] == (dpi, dpi)
|
||||||
|
|
||||||
|
@ -198,6 +214,7 @@ class TestFileTiff:
|
||||||
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
|
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
|
||||||
im.save(b, format="tiff", resolution=123.45)
|
im.save(b, format="tiff", resolution=123.45)
|
||||||
with Image.open(b) as im:
|
with Image.open(b) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2[X_RESOLUTION] == 123.45
|
assert im.tag_v2[X_RESOLUTION] == 123.45
|
||||||
assert im.tag_v2[Y_RESOLUTION] == 123.45
|
assert im.tag_v2[Y_RESOLUTION] == 123.45
|
||||||
|
|
||||||
|
@ -213,10 +230,12 @@ class TestFileTiff:
|
||||||
TiffImagePlugin.PREFIXES.pop()
|
TiffImagePlugin.PREFIXES.pop()
|
||||||
|
|
||||||
def test_bad_exif(self) -> None:
|
def test_bad_exif(self) -> None:
|
||||||
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
|
with Image.open("Tests/images/hopper_bad_exif.jpg") as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
|
|
||||||
# Should not raise struct.error.
|
# Should not raise struct.error.
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(UserWarning):
|
||||||
i._getexif()
|
im._getexif()
|
||||||
|
|
||||||
def test_save_rgba(self, tmp_path: Path) -> None:
|
def test_save_rgba(self, tmp_path: Path) -> None:
|
||||||
im = hopper("RGBA")
|
im = hopper("RGBA")
|
||||||
|
@ -307,11 +326,13 @@ class TestFileTiff:
|
||||||
)
|
)
|
||||||
def test_n_frames(self, path: str, n_frames: int) -> None:
|
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||||
with Image.open(path) as im:
|
with Image.open(path) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == n_frames
|
assert im.n_frames == n_frames
|
||||||
assert im.is_animated == (n_frames != 1)
|
assert im.is_animated == (n_frames != 1)
|
||||||
|
|
||||||
def test_eoferror(self) -> None:
|
def test_eoferror(self) -> None:
|
||||||
with Image.open("Tests/images/multipage-lastframe.tif") as im:
|
with Image.open("Tests/images/multipage-lastframe.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
n_frames = im.n_frames
|
n_frames = im.n_frames
|
||||||
|
|
||||||
# Test seeking past the last frame
|
# Test seeking past the last frame
|
||||||
|
@ -355,19 +376,24 @@ class TestFileTiff:
|
||||||
def test_frame_order(self) -> None:
|
def test_frame_order(self) -> None:
|
||||||
# A frame can't progress to itself after reading
|
# A frame can't progress to itself after reading
|
||||||
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
|
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
|
|
||||||
# A frame can't progress to a frame that has already been read
|
# A frame can't progress to a frame that has already been read
|
||||||
with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
|
with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
|
|
||||||
# Frames don't have to be in sequence
|
# Frames don't have to be in sequence
|
||||||
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
|
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 3
|
assert im.n_frames == 3
|
||||||
|
|
||||||
def test___str__(self) -> None:
|
def test___str__(self) -> None:
|
||||||
filename = "Tests/images/pil136.tiff"
|
filename = "Tests/images/pil136.tiff"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
ret = str(im.ifd)
|
ret = str(im.ifd)
|
||||||
|
|
||||||
|
@ -378,6 +404,8 @@ class TestFileTiff:
|
||||||
# Arrange
|
# Arrange
|
||||||
filename = "Tests/images/pil136.tiff"
|
filename = "Tests/images/pil136.tiff"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# v2 interface
|
# v2 interface
|
||||||
v2_tags = {
|
v2_tags = {
|
||||||
256: 55,
|
256: 55,
|
||||||
|
@ -417,6 +445,7 @@ class TestFileTiff:
|
||||||
def test__delitem__(self) -> None:
|
def test__delitem__(self) -> None:
|
||||||
filename = "Tests/images/pil136.tiff"
|
filename = "Tests/images/pil136.tiff"
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
len_before = len(dict(im.ifd))
|
len_before = len(dict(im.ifd))
|
||||||
del im.ifd[256]
|
del im.ifd[256]
|
||||||
len_after = len(dict(im.ifd))
|
len_after = len(dict(im.ifd))
|
||||||
|
@ -449,6 +478,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_ifd_tag_type(self) -> None:
|
def test_ifd_tag_type(self) -> None:
|
||||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert 0x8825 in im.tag_v2
|
assert 0x8825 in im.tag_v2
|
||||||
|
|
||||||
def test_exif(self, tmp_path: Path) -> None:
|
def test_exif(self, tmp_path: Path) -> None:
|
||||||
|
@ -537,6 +567,7 @@ class TestFileTiff:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(filename, tiffinfo={262: 0})
|
im.save(filename, tiffinfo={262: 0})
|
||||||
with Image.open(filename) as reloaded:
|
with Image.open(filename) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[262] == 0
|
assert reloaded.tag_v2[262] == 0
|
||||||
assert_image_equal(im, reloaded)
|
assert_image_equal(im, reloaded)
|
||||||
|
|
||||||
|
@ -615,6 +646,8 @@ class TestFileTiff:
|
||||||
filename = tmp_path / "temp.tif"
|
filename = tmp_path / "temp.tif"
|
||||||
hopper("RGB").save(filename, "TIFF", **kwargs)
|
hopper("RGB").save(filename, "TIFF", **kwargs)
|
||||||
with Image.open(filename) as im:
|
with Image.open(filename) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
|
|
||||||
# legacy interface
|
# legacy interface
|
||||||
assert im.tag[X_RESOLUTION][0][0] == 72
|
assert im.tag[X_RESOLUTION][0][0] == 72
|
||||||
assert im.tag[Y_RESOLUTION][0][0] == 36
|
assert im.tag[Y_RESOLUTION][0][0] == 36
|
||||||
|
@ -701,6 +734,7 @@ class TestFileTiff:
|
||||||
def test_planar_configuration_save(self, tmp_path: Path) -> None:
|
def test_planar_configuration_save(self, tmp_path: Path) -> None:
|
||||||
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
||||||
with Image.open(infile) as im:
|
with Image.open(infile) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im._planar_configuration == 2
|
assert im._planar_configuration == 2
|
||||||
|
|
||||||
outfile = tmp_path / "temp.tif"
|
outfile = tmp_path / "temp.tif"
|
||||||
|
@ -733,6 +767,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
mp.seek(0, os.SEEK_SET)
|
mp.seek(0, os.SEEK_SET)
|
||||||
with Image.open(mp) as im:
|
with Image.open(mp) as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.n_frames == 3
|
assert im.n_frames == 3
|
||||||
|
|
||||||
# Test appending images
|
# Test appending images
|
||||||
|
@ -743,6 +778,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
mp.seek(0, os.SEEK_SET)
|
mp.seek(0, os.SEEK_SET)
|
||||||
with Image.open(mp) as reread:
|
with Image.open(mp) as reread:
|
||||||
|
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
# Test appending using a generator
|
# Test appending using a generator
|
||||||
|
@ -754,6 +790,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
mp.seek(0, os.SEEK_SET)
|
mp.seek(0, os.SEEK_SET)
|
||||||
with Image.open(mp) as reread:
|
with Image.open(mp) as reread:
|
||||||
|
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||||
assert reread.n_frames == 3
|
assert reread.n_frames == 3
|
||||||
|
|
||||||
def test_fixoffsets(self) -> None:
|
def test_fixoffsets(self) -> None:
|
||||||
|
@ -864,6 +901,7 @@ class TestFileTiff:
|
||||||
|
|
||||||
def test_get_photoshop_blocks(self) -> None:
|
def test_get_photoshop_blocks(self) -> None:
|
||||||
with Image.open("Tests/images/lab.tif") as im:
|
with Image.open("Tests/images/lab.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert list(im.get_photoshop_blocks().keys()) == [
|
assert list(im.get_photoshop_blocks().keys()) == [
|
||||||
1061,
|
1061,
|
||||||
1002,
|
1002,
|
||||||
|
|
|
@ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None:
|
||||||
img.save(f, tiffinfo=info)
|
img.save(f, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
|
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||||
|
|
||||||
|
@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None:
|
||||||
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
||||||
img.save(f, tiffinfo=info)
|
img.save(f, tiffinfo=info)
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
|
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||||
|
|
||||||
|
|
||||||
def test_read_metadata() -> None:
|
def test_read_metadata() -> None:
|
||||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||||
|
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||||
assert {
|
assert {
|
||||||
"YResolution": IFDRational(4294967295, 113653537),
|
"YResolution": IFDRational(4294967295, 113653537),
|
||||||
"PlanarConfiguration": 1,
|
"PlanarConfiguration": 1,
|
||||||
|
@ -128,6 +131,7 @@ def test_read_metadata() -> None:
|
||||||
def test_write_metadata(tmp_path: Path) -> None:
|
def test_write_metadata(tmp_path: Path) -> None:
|
||||||
"""Test metadata writing through the python code"""
|
"""Test metadata writing through the python code"""
|
||||||
with Image.open("Tests/images/hopper.tif") as img:
|
with Image.open("Tests/images/hopper.tif") as img:
|
||||||
|
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||||
f = tmp_path / "temp.tiff"
|
f = tmp_path / "temp.tiff"
|
||||||
del img.tag[278]
|
del img.tag[278]
|
||||||
img.save(f, tiffinfo=img.tag)
|
img.save(f, tiffinfo=img.tag)
|
||||||
|
@ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None:
|
||||||
original = img.tag_v2.named()
|
original = img.tag_v2.named()
|
||||||
|
|
||||||
with Image.open(f) as loaded:
|
with Image.open(f) as loaded:
|
||||||
|
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||||
reloaded = loaded.tag_v2.named()
|
reloaded = loaded.tag_v2.named()
|
||||||
|
|
||||||
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
|
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
|
||||||
|
@ -165,6 +170,7 @@ def test_write_metadata(tmp_path: Path) -> None:
|
||||||
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
||||||
out = tmp_path / "temp.tiff"
|
out = tmp_path / "temp.tiff"
|
||||||
with Image.open("Tests/images/hopper.tif") as im:
|
with Image.open("Tests/images/hopper.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
info = im.tag_v2
|
info = im.tag_v2
|
||||||
del info[278]
|
del info[278]
|
||||||
|
|
||||||
|
@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
|
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
|
||||||
|
|
||||||
|
|
||||||
|
@ -231,6 +238,7 @@ def test_writing_other_types_to_ascii(
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[271] == expected
|
assert reloaded.tag_v2[271] == expected
|
||||||
|
|
||||||
|
|
||||||
|
@ -248,6 +256,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path)
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[700] == b"\x01"
|
assert reloaded.tag_v2[700] == b"\x01"
|
||||||
|
|
||||||
|
|
||||||
|
@ -267,6 +276,7 @@ def test_writing_other_types_to_undefined(
|
||||||
im.save(out, tiffinfo=info)
|
im.save(out, tiffinfo=info)
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[33723] == b"1"
|
assert reloaded.tag_v2[33723] == b"1"
|
||||||
|
|
||||||
|
|
||||||
|
@ -311,6 +321,7 @@ def test_iccprofile_binary() -> None:
|
||||||
# but probably won't be able to save it.
|
# but probably won't be able to save it.
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert im.tag_v2.tagtype[34675] == 1
|
assert im.tag_v2.tagtype[34675] == 1
|
||||||
assert im.info["icc_profile"]
|
assert im.info["icc_profile"]
|
||||||
|
|
||||||
|
@ -336,6 +347,7 @@ def test_exif_div_zero(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert 0 == reloaded.tag_v2[41988].numerator
|
assert 0 == reloaded.tag_v2[41988].numerator
|
||||||
assert 0 == reloaded.tag_v2[41988].denominator
|
assert 0 == reloaded.tag_v2[41988].denominator
|
||||||
|
|
||||||
|
@ -355,6 +367,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert max_long == reloaded.tag_v2[41493].numerator
|
assert max_long == reloaded.tag_v2[41493].numerator
|
||||||
assert 1 == reloaded.tag_v2[41493].denominator
|
assert 1 == reloaded.tag_v2[41493].denominator
|
||||||
|
|
||||||
|
@ -367,6 +380,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert max_long == reloaded.tag_v2[41493].numerator
|
assert max_long == reloaded.tag_v2[41493].numerator
|
||||||
assert 1 == reloaded.tag_v2[41493].denominator
|
assert 1 == reloaded.tag_v2[41493].denominator
|
||||||
|
|
||||||
|
@ -385,6 +399,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert numerator == reloaded.tag_v2[37380].numerator
|
assert numerator == reloaded.tag_v2[37380].numerator
|
||||||
assert denominator == reloaded.tag_v2[37380].denominator
|
assert denominator == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
|
@ -397,6 +412,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert numerator == reloaded.tag_v2[37380].numerator
|
assert numerator == reloaded.tag_v2[37380].numerator
|
||||||
assert denominator == reloaded.tag_v2[37380].denominator
|
assert denominator == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
|
@ -410,6 +426,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
||||||
assert -1 == reloaded.tag_v2[37380].denominator
|
assert -1 == reloaded.tag_v2[37380].denominator
|
||||||
|
|
||||||
|
@ -424,6 +441,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None:
|
||||||
im.save(out, tiffinfo=info, compression="raw")
|
im.save(out, tiffinfo=info, compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert reloaded.tag_v2[37000] == -60000
|
assert reloaded.tag_v2[37000] == -60000
|
||||||
|
|
||||||
|
|
||||||
|
@ -444,11 +462,13 @@ def test_empty_values() -> None:
|
||||||
|
|
||||||
def test_photoshop_info(tmp_path: Path) -> None:
|
def test_photoshop_info(tmp_path: Path) -> None:
|
||||||
with Image.open("Tests/images/issue_2278.tif") as im:
|
with Image.open("Tests/images/issue_2278.tif") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
assert len(im.tag_v2[34377]) == 70
|
assert len(im.tag_v2[34377]) == 70
|
||||||
assert isinstance(im.tag_v2[34377], bytes)
|
assert isinstance(im.tag_v2[34377], bytes)
|
||||||
out = tmp_path / "temp.tiff"
|
out = tmp_path / "temp.tiff"
|
||||||
im.save(out)
|
im.save(out)
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert len(reloaded.tag_v2[34377]) == 70
|
assert len(reloaded.tag_v2[34377]) == 70
|
||||||
assert isinstance(reloaded.tag_v2[34377], bytes)
|
assert isinstance(reloaded.tag_v2[34377], bytes)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
from packaging.version import parse as parse_version
|
from packaging.version import parse as parse_version
|
||||||
|
|
||||||
from PIL import Image, features
|
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
assert_image_equal,
|
assert_image_equal,
|
||||||
|
@ -22,10 +22,12 @@ def test_n_frames() -> None:
|
||||||
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
|
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
|
||||||
|
|
||||||
with Image.open("Tests/images/hopper.webp") as im:
|
with Image.open("Tests/images/hopper.webp") as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 1
|
assert im.n_frames == 1
|
||||||
assert not im.is_animated
|
assert not im.is_animated
|
||||||
|
|
||||||
with Image.open("Tests/images/iss634.webp") as im:
|
with Image.open("Tests/images/iss634.webp") as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 42
|
assert im.n_frames == 42
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Image.open("Tests/images/iss634.gif") as orig:
|
with Image.open("Tests/images/iss634.gif") as orig:
|
||||||
|
assert isinstance(orig, GifImagePlugin.GifImageFile)
|
||||||
assert orig.n_frames > 1
|
assert orig.n_frames > 1
|
||||||
|
|
||||||
temp_file = tmp_path / "temp.webp"
|
temp_file = tmp_path / "temp.webp"
|
||||||
orig.save(temp_file, save_all=True)
|
orig.save(temp_file, save_all=True)
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == orig.n_frames
|
assert im.n_frames == orig.n_frames
|
||||||
|
|
||||||
# Compare first and last frames to the original animated GIF
|
# Compare first and last frames to the original animated GIF
|
||||||
|
@ -69,6 +73,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def check(temp_file: Path) -> None:
|
def check(temp_file: Path) -> None:
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 2
|
assert im.n_frames == 2
|
||||||
|
|
||||||
# Compare first frame to original
|
# Compare first frame to original
|
||||||
|
@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 5
|
assert im.n_frames == 5
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with Image.open(temp_file) as im:
|
with Image.open(temp_file) as im:
|
||||||
|
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||||
assert im.n_frames == 5
|
assert im.n_frames == 5
|
||||||
assert im.is_animated
|
assert im.is_animated
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from types import ModuleType
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image, WebPImagePlugin
|
||||||
|
|
||||||
from .helper import mark_if_feature_version, skip_unless_feature
|
from .helper import mark_if_feature_version, skip_unless_feature
|
||||||
|
|
||||||
|
@ -110,6 +110,7 @@ def test_read_no_exif() -> None:
|
||||||
|
|
||||||
test_buffer.seek(0)
|
test_buffer.seek(0)
|
||||||
with Image.open(test_buffer) as webp_image:
|
with Image.open(test_buffer) as webp_image:
|
||||||
|
assert isinstance(webp_image, WebPImagePlugin.WebPImageFile)
|
||||||
assert not webp_image._getexif()
|
assert not webp_image._getexif()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,7 @@ def test_load_float_dpi() -> None:
|
||||||
|
|
||||||
def test_load_set_dpi() -> None:
|
def test_load_set_dpi() -> None:
|
||||||
with Image.open("Tests/images/drawing.wmf") as im:
|
with Image.open("Tests/images/drawing.wmf") as im:
|
||||||
|
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||||
assert im.size == (82, 82)
|
assert im.size == (82, 82)
|
||||||
|
|
||||||
if hasattr(Image.core, "drawwmf"):
|
if hasattr(Image.core, "drawwmf"):
|
||||||
|
@ -102,10 +103,12 @@ def test_load_set_dpi() -> None:
|
||||||
|
|
||||||
if not hasattr(Image.core, "drawwmf"):
|
if not hasattr(Image.core, "drawwmf"):
|
||||||
return
|
return
|
||||||
|
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||||
im.load(im.info["dpi"])
|
im.load(im.info["dpi"])
|
||||||
assert im.size == (1625, 1625)
|
assert im.size == (1625, 1625)
|
||||||
|
|
||||||
with Image.open("Tests/images/drawing.emf") as im:
|
with Image.open("Tests/images/drawing.emf") as im:
|
||||||
|
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||||
im.load((72, 144))
|
im.load((72, 144))
|
||||||
assert im.size == (82, 164)
|
assert im.size == (82, 164)
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ def test_invalid_file() -> None:
|
||||||
def test_load_read() -> None:
|
def test_load_read() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open(TEST_FILE) as im:
|
with Image.open(TEST_FILE) as im:
|
||||||
|
assert isinstance(im, XpmImagePlugin.XpmImageFile)
|
||||||
dummy_bytes = 1
|
dummy_bytes = 1
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
|
|
|
@ -230,8 +230,8 @@ class TestImage:
|
||||||
assert_image_similar(im, reloaded, 20)
|
assert_image_similar(im, reloaded, 20)
|
||||||
|
|
||||||
def test_unknown_extension(self, tmp_path: Path) -> None:
|
def test_unknown_extension(self, tmp_path: Path) -> None:
|
||||||
im = hopper()
|
|
||||||
temp_file = tmp_path / "temp.unknown"
|
temp_file = tmp_path / "temp.unknown"
|
||||||
|
with hopper() as im:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
im.save(temp_file)
|
im.save(temp_file)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageSequence, TiffImagePlugin
|
from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal, hopper, skip_unless_feature
|
from .helper import assert_image_equal, hopper, skip_unless_feature
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ def test_sanity(tmp_path: Path) -> None:
|
||||||
|
|
||||||
def test_iterator() -> None:
|
def test_iterator() -> None:
|
||||||
with Image.open("Tests/images/multipage.tiff") as im:
|
with Image.open("Tests/images/multipage.tiff") as im:
|
||||||
|
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||||
i = ImageSequence.Iterator(im)
|
i = ImageSequence.Iterator(im)
|
||||||
for index in range(im.n_frames):
|
for index in range(im.n_frames):
|
||||||
assert i[index] == next(i)
|
assert i[index] == next(i)
|
||||||
|
@ -42,6 +43,7 @@ def test_iterator() -> None:
|
||||||
|
|
||||||
def test_iterator_min_frame() -> None:
|
def test_iterator_min_frame() -> None:
|
||||||
with Image.open("Tests/images/hopper.psd") as im:
|
with Image.open("Tests/images/hopper.psd") as im:
|
||||||
|
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||||
i = ImageSequence.Iterator(im)
|
i = ImageSequence.Iterator(im)
|
||||||
for index in range(1, im.n_frames):
|
for index in range(1, im.n_frames):
|
||||||
assert i[index] == next(i)
|
assert i[index] == next(i)
|
||||||
|
|
112
Tests/test_pyarrow.py
Normal file
112
Tests/test_pyarrow.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any # undone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from .helper import (
|
||||||
|
assert_deep_equal,
|
||||||
|
assert_image_equal,
|
||||||
|
hopper,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed")
|
||||||
|
|
||||||
|
TEST_IMAGE_SIZE = (10, 10)
|
||||||
|
|
||||||
|
|
||||||
|
def _test_img_equals_pyarray(
|
||||||
|
img: Image.Image, arr: Any, mask: list[int] | None
|
||||||
|
) -> None:
|
||||||
|
assert img.height * img.width == len(arr)
|
||||||
|
px = img.load()
|
||||||
|
assert px is not None
|
||||||
|
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:
|
||||||
|
for ix, elt in enumerate(mask):
|
||||||
|
pixel = px[x, y]
|
||||||
|
assert isinstance(pixel, tuple)
|
||||||
|
assert pixel[ix] == arr[y * img.width + x].as_py()[elt]
|
||||||
|
else:
|
||||||
|
assert_deep_equal(px[x, y], arr[y * img.width + x].as_py())
|
||||||
|
|
||||||
|
|
||||||
|
# really hard to get a non-nullable list type
|
||||||
|
fl_uint8_4_type = pyarrow.field(
|
||||||
|
"_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4)
|
||||||
|
).type
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mode, dtype, mask",
|
||||||
|
(
|
||||||
|
("L", pyarrow.uint8(), None),
|
||||||
|
("I", pyarrow.int32(), None),
|
||||||
|
("F", pyarrow.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: Any, 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 = pyarrow.array(img)
|
||||||
|
_test_img_equals_pyarray(img, arr, mask)
|
||||||
|
assert arr.type == dtype
|
||||||
|
|
||||||
|
reloaded = Image.fromarrow(arr, mode, img.size)
|
||||||
|
|
||||||
|
assert reloaded
|
||||||
|
|
||||||
|
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 = pyarrow.array(img)
|
||||||
|
arr_2 = pyarrow.array(img)
|
||||||
|
|
||||||
|
del img
|
||||||
|
|
||||||
|
assert arr_1.sum().as_py() > 0
|
||||||
|
del arr_1
|
||||||
|
|
||||||
|
assert arr_2.sum().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 = pyarrow.array(img)
|
||||||
|
arr_2 = pyarrow.array(img)
|
||||||
|
|
||||||
|
assert arr_1.sum().as_py() > 0
|
||||||
|
del arr_1
|
||||||
|
|
||||||
|
assert arr_2.sum().as_py() > 0
|
||||||
|
del arr_2
|
||||||
|
|
||||||
|
img2 = img.copy()
|
||||||
|
px = img2.load()
|
||||||
|
assert px # make mypy happy
|
||||||
|
assert isinstance(px[0, 0], int)
|
|
@ -39,6 +39,7 @@ class TestShellInjection:
|
||||||
shutil.copy(TEST_JPG, src_file)
|
shutil.copy(TEST_JPG, src_file)
|
||||||
|
|
||||||
with Image.open(src_file) as im:
|
with Image.open(src_file) as im:
|
||||||
|
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||||
im.load_djpeg()
|
im.load_djpeg()
|
||||||
|
|
||||||
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
|
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")
|
||||||
|
|
|
@ -72,4 +72,5 @@ def test_ifd_rational_save(
|
||||||
im.save(out, dpi=(res, res), compression="raw")
|
im.save(out, dpi=(res, res), compression="raw")
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
|
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||||
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
|
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
|
||||||
|
|
64
depends/install_libavif.sh
Executable file
64
depends/install_libavif.sh
Executable file
|
@ -0,0 +1,64 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
version=1.2.1
|
||||||
|
|
||||||
|
./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz
|
||||||
|
|
||||||
|
pushd libavif-$version
|
||||||
|
|
||||||
|
if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then
|
||||||
|
PREFIX=$(brew --prefix)
|
||||||
|
else
|
||||||
|
PREFIX=/usr
|
||||||
|
fi
|
||||||
|
|
||||||
|
PKGCONFIG=${PKGCONFIG:-pkg-config}
|
||||||
|
|
||||||
|
LIBAVIF_CMAKE_FLAGS=()
|
||||||
|
HAS_DECODER=0
|
||||||
|
HAS_ENCODER=0
|
||||||
|
|
||||||
|
if $PKGCONFIG --exists aom; then
|
||||||
|
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM)
|
||||||
|
HAS_ENCODER=1
|
||||||
|
HAS_DECODER=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $PKGCONFIG --exists dav1d; then
|
||||||
|
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM)
|
||||||
|
HAS_DECODER=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $PKGCONFIG --exists libgav1; then
|
||||||
|
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM)
|
||||||
|
HAS_DECODER=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $PKGCONFIG --exists rav1e; then
|
||||||
|
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM)
|
||||||
|
HAS_ENCODER=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $PKGCONFIG --exists SvtAv1Enc; then
|
||||||
|
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM)
|
||||||
|
HAS_ENCODER=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then
|
||||||
|
LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmake \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=$PREFIX \
|
||||||
|
-DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_MACOSX_RPATH=OFF \
|
||||||
|
-DAVIF_LIBSHARPYUV=LOCAL \
|
||||||
|
-DAVIF_LIBYUV=LOCAL \
|
||||||
|
"${LIBAVIF_CMAKE_FLAGS[@]}" \
|
||||||
|
.
|
||||||
|
|
||||||
|
sudo make install
|
||||||
|
|
||||||
|
popd
|
|
@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
|
||||||
Fully supported formats
|
Fully supported formats
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
AVIF
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
Pillow reads and writes AVIF files, including AVIF sequence images.
|
||||||
|
It is only possible to save 8-bit AVIF images, and all AVIF images are decoded
|
||||||
|
as 8-bit RGB(A).
|
||||||
|
|
||||||
|
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
||||||
|
|
||||||
|
**quality**
|
||||||
|
Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest
|
||||||
|
quality, 100 the largest size and best quality.
|
||||||
|
|
||||||
|
**subsampling**
|
||||||
|
If present, sets the subsampling for the encoder. Defaults to ``4:2:0``.
|
||||||
|
Options include:
|
||||||
|
|
||||||
|
* ``4:0:0``
|
||||||
|
* ``4:2:0``
|
||||||
|
* ``4:2:2``
|
||||||
|
* ``4:4:4``
|
||||||
|
|
||||||
|
**speed**
|
||||||
|
Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6.
|
||||||
|
|
||||||
|
**max_threads**
|
||||||
|
Limit the number of active threads used. By default, there is no limit. If the aom
|
||||||
|
codec is used, there is a maximum of 64.
|
||||||
|
|
||||||
|
**range**
|
||||||
|
YUV range, either "full" or "limited". Defaults to "full".
|
||||||
|
|
||||||
|
**codec**
|
||||||
|
AV1 codec to use for encoding. Specific values are "aom", "rav1e", and
|
||||||
|
"svt", presuming the chosen codec is available. Defaults to "auto", which
|
||||||
|
will choose the first available codec in the order of the preceding list.
|
||||||
|
|
||||||
|
**tile_rows** / **tile_cols**
|
||||||
|
For tile encoding, the (log 2) number of tile rows and columns to use.
|
||||||
|
Valid values are 0-6, default 0. Ignored if "autotiling" is set to true.
|
||||||
|
|
||||||
|
**autotiling**
|
||||||
|
Split the image up to allow parallelization. Enabled automatically if "tile_rows"
|
||||||
|
and "tile_cols" both have their default values of zero.
|
||||||
|
|
||||||
|
**alpha_premultiplied**
|
||||||
|
Encode the image with premultiplied alpha. Defaults to ``False``.
|
||||||
|
|
||||||
|
**advanced**
|
||||||
|
Codec specific options.
|
||||||
|
|
||||||
|
**icc_profile**
|
||||||
|
The ICC Profile to include in the saved file.
|
||||||
|
|
||||||
|
**exif**
|
||||||
|
The exif data to include in the saved file.
|
||||||
|
|
||||||
|
**xmp**
|
||||||
|
The XMP data to include in the saved file.
|
||||||
|
|
||||||
|
Saving sequences
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default
|
||||||
|
only the first frame of a multiframe image will be saved. If the ``save_all``
|
||||||
|
argument is present and true, then all frames will be saved, and the following
|
||||||
|
options will also be available.
|
||||||
|
|
||||||
|
**append_images**
|
||||||
|
A list of images to append as additional frames. Each of the
|
||||||
|
images in the list can be single or multiframe images.
|
||||||
|
|
||||||
|
**duration**
|
||||||
|
The display duration of each frame, in milliseconds. Pass a single
|
||||||
|
integer for a constant duration, or a list or tuple to set the
|
||||||
|
duration for each frame separately.
|
||||||
|
|
||||||
BLP
|
BLP
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
|
@ -242,7 +319,7 @@ following options are available::
|
||||||
**append_images**
|
**append_images**
|
||||||
A list of images to append as additional frames. Each of the
|
A list of images to append as additional frames. Each of the
|
||||||
images in the list can be single or multiframe images.
|
images in the list can be single or multiframe images.
|
||||||
This is currently supported for GIF, PDF, PNG, TIFF, and WebP.
|
This is supported for AVIF, GIF, PDF, PNG, TIFF and WebP.
|
||||||
|
|
||||||
It is also supported for ICO and ICNS. If images are passed in of relevant
|
It is also supported for ICO and ICNS. If images are passed in of relevant
|
||||||
sizes, they will be used instead of scaling down the main image.
|
sizes, they will be used instead of scaling down the main image.
|
||||||
|
|
|
@ -89,6 +89,14 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libxcb** provides X11 screengrab support.
|
* **libxcb** provides X11 screengrab support.
|
||||||
|
|
||||||
|
* **libavif** provides support for the AVIF format.
|
||||||
|
|
||||||
|
* Pillow requires libavif version **1.0.0** or greater.
|
||||||
|
* libavif is merely an API that wraps AVIF codecs. If you are compiling
|
||||||
|
libavif from source, you will also need to install both an AVIF encoder
|
||||||
|
and decoder, such as rav1e and dav1d, or libaom, which both encodes and
|
||||||
|
decodes AVIF images.
|
||||||
|
|
||||||
.. tab:: Linux
|
.. tab:: Linux
|
||||||
|
|
||||||
If you didn't build Python from source, make sure you have Python's
|
If you didn't build Python from source, make sure you have Python's
|
||||||
|
@ -117,6 +125,12 @@ Many of Pillow's features require external libraries:
|
||||||
To install libraqm, ``sudo apt-get install meson`` and then see
|
To install libraqm, ``sudo apt-get install meson`` and then see
|
||||||
``depends/install_raqm.sh``.
|
``depends/install_raqm.sh``.
|
||||||
|
|
||||||
|
Build prerequisites for libavif on Ubuntu are installed with::
|
||||||
|
|
||||||
|
sudo apt-get install cmake ninja-build nasm
|
||||||
|
|
||||||
|
Then see ``depends/install_libavif.sh`` to build and install libavif.
|
||||||
|
|
||||||
Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
|
Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
|
||||||
|
|
||||||
sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
|
sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
|
||||||
|
@ -148,7 +162,15 @@ Many of Pillow's features require external libraries:
|
||||||
The easiest way to install external libraries is via `Homebrew
|
The easiest way to install external libraries is via `Homebrew
|
||||||
<https://brew.sh/>`_. After you install Homebrew, run::
|
<https://brew.sh/>`_. After you install Homebrew, run::
|
||||||
|
|
||||||
brew install libjpeg libraqm libtiff little-cms2 openjpeg webp
|
brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp
|
||||||
|
|
||||||
|
If you would like to use libavif with more codecs than just aom, then
|
||||||
|
instead of installing libavif through Homebrew directly, you can use
|
||||||
|
Homebrew to install libavif's build dependencies::
|
||||||
|
|
||||||
|
brew install aom dav1d rav1e svt-av1
|
||||||
|
|
||||||
|
Then see ``depends/install_libavif.sh`` to install libavif.
|
||||||
|
|
||||||
.. tab:: Windows
|
.. tab:: Windows
|
||||||
|
|
||||||
|
@ -187,7 +209,8 @@ Many of Pillow's features require external libraries:
|
||||||
mingw-w64-x86_64-libwebp \
|
mingw-w64-x86_64-libwebp \
|
||||||
mingw-w64-x86_64-openjpeg2 \
|
mingw-w64-x86_64-openjpeg2 \
|
||||||
mingw-w64-x86_64-libimagequant \
|
mingw-w64-x86_64-libimagequant \
|
||||||
mingw-w64-x86_64-libraqm
|
mingw-w64-x86_64-libraqm \
|
||||||
|
mingw-w64-x86_64-libavif
|
||||||
|
|
||||||
.. tab:: FreeBSD
|
.. tab:: FreeBSD
|
||||||
|
|
||||||
|
@ -199,7 +222,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
Prerequisites are installed on **FreeBSD 10 or 11** with::
|
Prerequisites are installed on **FreeBSD 10 or 11** with::
|
||||||
|
|
||||||
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
|
sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif
|
||||||
|
|
||||||
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
|
Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,7 @@ Constructing images
|
||||||
|
|
||||||
.. autofunction:: new
|
.. autofunction:: new
|
||||||
.. autofunction:: fromarray
|
.. autofunction:: fromarray
|
||||||
|
.. autofunction:: fromarrow
|
||||||
.. autofunction:: frombytes
|
.. autofunction:: frombytes
|
||||||
.. autofunction:: frombuffer
|
.. autofunction:: frombuffer
|
||||||
|
|
||||||
|
@ -370,6 +371,8 @@ Protocols
|
||||||
|
|
||||||
.. autoclass:: SupportsArrayInterface
|
.. autoclass:: SupportsArrayInterface
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
.. autoclass:: SupportsArrowArrayInterface
|
||||||
|
:show-inheritance:
|
||||||
.. autoclass:: SupportsGetData
|
.. autoclass:: SupportsGetData
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
88
docs/reference/arrow_support.rst
Normal file
88
docs/reference/arrow_support.rst
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
.. _arrow-support:
|
||||||
|
|
||||||
|
=============
|
||||||
|
Arrow Support
|
||||||
|
=============
|
||||||
|
|
||||||
|
`Arrow <https://arrow.apache.org/>`__
|
||||||
|
is an in-memory data exchange format that is the spiritual
|
||||||
|
successor to the NumPy array interface. It provides for zero-copy
|
||||||
|
access to columnar data, which in our case is ``Image`` data.
|
||||||
|
|
||||||
|
The goal with Arrow is to provide native zero-copy interoperability
|
||||||
|
with any Arrow provider or consumer in the Python ecosystem.
|
||||||
|
|
||||||
|
.. warning:: Zero-copy does not mean zero allocation -- the internal
|
||||||
|
memory layout of Pillow images contains an allocation for row
|
||||||
|
pointers, so there is a non-zero, but significantly smaller than a
|
||||||
|
full-copy memory cost to reading an Arrow image.
|
||||||
|
|
||||||
|
|
||||||
|
Data Formats
|
||||||
|
============
|
||||||
|
|
||||||
|
Pillow currently supports exporting Arrow images in all modes
|
||||||
|
**except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to
|
||||||
|
line-length packing in these modes making for non-continuous memory.
|
||||||
|
|
||||||
|
For single-band images, the exported array is width*height elements,
|
||||||
|
with each pixel corresponding to the appropriate Arrow type.
|
||||||
|
|
||||||
|
For multiband images, the exported array is width*height fixed-length
|
||||||
|
four-element arrays of uint8. This is memory compatible with the raw
|
||||||
|
image storage of four bytes per pixel.
|
||||||
|
|
||||||
|
Mode ``1`` images are exported as one uint8 byte/pixel, as this is
|
||||||
|
consistent with the internal storage.
|
||||||
|
|
||||||
|
Pillow will accept, but not produce, one other format. For any
|
||||||
|
multichannel image with 32-bit storage per pixel, Pillow will accept
|
||||||
|
an array of width*height int32 elements, which will then be
|
||||||
|
interpreted using the mode-specific interpretation of the bytes.
|
||||||
|
|
||||||
|
The image mode must match the Arrow band format when reading single
|
||||||
|
channel images.
|
||||||
|
|
||||||
|
Memory Allocator
|
||||||
|
================
|
||||||
|
|
||||||
|
Pillow's default memory allocator, the :ref:`block_allocator`,
|
||||||
|
allocates up to a 16 MB block for images by default. Larger images
|
||||||
|
overflow into additional blocks. Arrow requires a single continuous
|
||||||
|
memory allocation, so images allocated in multiple blocks cannot be
|
||||||
|
exported in the Arrow format.
|
||||||
|
|
||||||
|
To enable the single block allocator::
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
Image.core.set_use_block_allocator(1)
|
||||||
|
|
||||||
|
Note that this is a global setting, not a per-image setting.
|
||||||
|
|
||||||
|
Unsupported Features
|
||||||
|
====================
|
||||||
|
|
||||||
|
* Table/dataframe protocol. We support a single array.
|
||||||
|
* Null markers, producing or consuming. Null values are inferred from
|
||||||
|
the mode, e.g. RGB images are stored in the first three bytes of
|
||||||
|
each 32-bit pixel, and the last byte is an implied null.
|
||||||
|
* Schema negotiation. There is an optional schema for the requested
|
||||||
|
datatype in the Arrow source interface. We ignore that
|
||||||
|
parameter.
|
||||||
|
* Array metadata.
|
||||||
|
|
||||||
|
Internal Details
|
||||||
|
================
|
||||||
|
|
||||||
|
Python Arrow C interface:
|
||||||
|
https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html
|
||||||
|
|
||||||
|
The memory that is exported from the Arrow interface is shared -- not
|
||||||
|
copied, so the lifetime of the memory allocation is no longer strictly
|
||||||
|
tied to the life of the Python object.
|
||||||
|
|
||||||
|
The core imaging struct now has a refcount associated with it, and the
|
||||||
|
lifetime of the core image struct is now divorced from the Python
|
||||||
|
image object. Creating an arrow reference to the image increments the
|
||||||
|
refcount, and the imaging struct is only released when the refcount
|
||||||
|
reaches zero.
|
|
@ -1,3 +1,6 @@
|
||||||
|
|
||||||
|
.. _block_allocator:
|
||||||
|
|
||||||
Block Allocator
|
Block Allocator
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ Support for the following modules can be checked:
|
||||||
* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`.
|
* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`.
|
||||||
* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`.
|
* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`.
|
||||||
* ``webp``: WebP image support.
|
* ``webp``: WebP image support.
|
||||||
|
* ``avif``: AVIF image support.
|
||||||
|
|
||||||
.. autofunction:: PIL.features.check_module
|
.. autofunction:: PIL.features.check_module
|
||||||
.. autofunction:: PIL.features.version_module
|
.. autofunction:: PIL.features.version_module
|
||||||
|
|
|
@ -9,3 +9,4 @@ Internal Reference
|
||||||
block_allocator
|
block_allocator
|
||||||
internal_modules
|
internal_modules
|
||||||
c_extension_debugging
|
c_extension_debugging
|
||||||
|
arrow_support
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
Plugin reference
|
Plugin reference
|
||||||
================
|
================
|
||||||
|
|
||||||
|
:mod:`~PIL.AvifImagePlugin` Module
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: PIL.AvifImagePlugin
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
:mod:`~PIL.BmpImagePlugin` Module
|
:mod:`~PIL.BmpImagePlugin` Module
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
|
|
|
@ -68,3 +68,12 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1,
|
||||||
DXT5, BC2, BC3 and BC5 are supported::
|
DXT5, BC2, BC3 and BC5 are supported::
|
||||||
|
|
||||||
im.save("out.dds", pixel_format="DXT1")
|
im.save("out.dds", pixel_format="DXT1")
|
||||||
|
|
||||||
|
Other Changes
|
||||||
|
=============
|
||||||
|
|
||||||
|
Reading and writing AVIF images
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Pillow can now read and write AVIF images. If you are building Pillow from source, this
|
||||||
|
will require libavif 1.0.0 or later.
|
||||||
|
|
|
@ -54,6 +54,10 @@ optional-dependencies.fpx = [
|
||||||
optional-dependencies.mic = [
|
optional-dependencies.mic = [
|
||||||
"olefile",
|
"olefile",
|
||||||
]
|
]
|
||||||
|
optional-dependencies.test-arrow = [
|
||||||
|
"pyarrow",
|
||||||
|
]
|
||||||
|
|
||||||
optional-dependencies.tests = [
|
optional-dependencies.tests = [
|
||||||
"check-manifest",
|
"check-manifest",
|
||||||
"coverage>=7.4.2",
|
"coverage>=7.4.2",
|
||||||
|
@ -67,6 +71,7 @@ optional-dependencies.tests = [
|
||||||
"pytest-timeout",
|
"pytest-timeout",
|
||||||
"trove-classifiers>=2024.10.12",
|
"trove-classifiers>=2024.10.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
optional-dependencies.typing = [
|
optional-dependencies.typing = [
|
||||||
"typing-extensions; python_version<'3.10'",
|
"typing-extensions; python_version<'3.10'",
|
||||||
]
|
]
|
||||||
|
|
20
setup.py
20
setup.py
|
@ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
|
||||||
PILLOW_VERSION = get_version()
|
PILLOW_VERSION = get_version()
|
||||||
|
AVIF_ROOT = None
|
||||||
FREETYPE_ROOT = None
|
FREETYPE_ROOT = None
|
||||||
HARFBUZZ_ROOT = None
|
HARFBUZZ_ROOT = None
|
||||||
FRIBIDI_ROOT = None
|
FRIBIDI_ROOT = None
|
||||||
|
@ -64,6 +65,7 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path")
|
||||||
_LIB_IMAGING = (
|
_LIB_IMAGING = (
|
||||||
"Access",
|
"Access",
|
||||||
"AlphaComposite",
|
"AlphaComposite",
|
||||||
|
"Arrow",
|
||||||
"Resample",
|
"Resample",
|
||||||
"Reduce",
|
"Reduce",
|
||||||
"Bands",
|
"Bands",
|
||||||
|
@ -306,6 +308,7 @@ class pil_build_ext(build_ext):
|
||||||
"jpeg2000",
|
"jpeg2000",
|
||||||
"imagequant",
|
"imagequant",
|
||||||
"xcb",
|
"xcb",
|
||||||
|
"avif",
|
||||||
]
|
]
|
||||||
|
|
||||||
required = {"jpeg", "zlib"}
|
required = {"jpeg", "zlib"}
|
||||||
|
@ -481,6 +484,7 @@ class pil_build_ext(build_ext):
|
||||||
#
|
#
|
||||||
# add configured kits
|
# add configured kits
|
||||||
for root_name, lib_name in {
|
for root_name, lib_name in {
|
||||||
|
"AVIF_ROOT": "avif",
|
||||||
"JPEG_ROOT": "libjpeg",
|
"JPEG_ROOT": "libjpeg",
|
||||||
"JPEG2K_ROOT": "libopenjp2",
|
"JPEG2K_ROOT": "libopenjp2",
|
||||||
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),
|
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),
|
||||||
|
@ -846,6 +850,12 @@ class pil_build_ext(build_ext):
|
||||||
if _find_library_file(self, "xcb"):
|
if _find_library_file(self, "xcb"):
|
||||||
feature.set("xcb", "xcb")
|
feature.set("xcb", "xcb")
|
||||||
|
|
||||||
|
if feature.want("avif"):
|
||||||
|
_dbg("Looking for avif")
|
||||||
|
if _find_include_file(self, "avif/avif.h"):
|
||||||
|
if _find_library_file(self, "avif"):
|
||||||
|
feature.set("avif", "avif")
|
||||||
|
|
||||||
for f in feature:
|
for f in feature:
|
||||||
if not feature.get(f) and feature.require(f):
|
if not feature.get(f) and feature.require(f):
|
||||||
if f in ("jpeg", "zlib"):
|
if f in ("jpeg", "zlib"):
|
||||||
|
@ -934,6 +944,14 @@ class pil_build_ext(build_ext):
|
||||||
else:
|
else:
|
||||||
self._remove_extension("PIL._webp")
|
self._remove_extension("PIL._webp")
|
||||||
|
|
||||||
|
if feature.get("avif"):
|
||||||
|
libs = [feature.get("avif")]
|
||||||
|
if sys.platform == "win32":
|
||||||
|
libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"])
|
||||||
|
self._update_extension("PIL._avif", libs)
|
||||||
|
else:
|
||||||
|
self._remove_extension("PIL._avif")
|
||||||
|
|
||||||
tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else []
|
tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else []
|
||||||
self._update_extension("PIL._imagingtk", tk_libs)
|
self._update_extension("PIL._imagingtk", tk_libs)
|
||||||
|
|
||||||
|
@ -976,6 +994,7 @@ class pil_build_ext(build_ext):
|
||||||
(feature.get("lcms"), "LITTLECMS2"),
|
(feature.get("lcms"), "LITTLECMS2"),
|
||||||
(feature.get("webp"), "WEBP"),
|
(feature.get("webp"), "WEBP"),
|
||||||
(feature.get("xcb"), "XCB (X protocol)"),
|
(feature.get("xcb"), "XCB (X protocol)"),
|
||||||
|
(feature.get("avif"), "LIBAVIF"),
|
||||||
]
|
]
|
||||||
|
|
||||||
all = 1
|
all = 1
|
||||||
|
@ -1018,6 +1037,7 @@ ext_modules = [
|
||||||
Extension("PIL._imagingft", ["src/_imagingft.c"]),
|
Extension("PIL._imagingft", ["src/_imagingft.c"]),
|
||||||
Extension("PIL._imagingcms", ["src/_imagingcms.c"]),
|
Extension("PIL._imagingcms", ["src/_imagingcms.c"]),
|
||||||
Extension("PIL._webp", ["src/_webp.c"]),
|
Extension("PIL._webp", ["src/_webp.c"]),
|
||||||
|
Extension("PIL._avif", ["src/_avif.c"]),
|
||||||
Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]),
|
Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]),
|
||||||
Extension("PIL._imagingmath", ["src/_imagingmath.c"]),
|
Extension("PIL._imagingmath", ["src/_imagingmath.c"]),
|
||||||
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
|
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
|
||||||
|
|
292
src/PIL/AvifImagePlugin.py
Normal file
292
src/PIL/AvifImagePlugin.py
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
from . import ExifTags, Image, ImageFile
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import _avif
|
||||||
|
|
||||||
|
SUPPORTED = True
|
||||||
|
except ImportError:
|
||||||
|
SUPPORTED = False
|
||||||
|
|
||||||
|
# Decoder options as module globals, until there is a way to pass parameters
|
||||||
|
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
|
||||||
|
DECODE_CODEC_CHOICE = "auto"
|
||||||
|
# Decoding is only affected by this for libavif **0.8.4** or greater.
|
||||||
|
DEFAULT_MAX_THREADS = 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_codec_version(codec_name: str) -> str | None:
|
||||||
|
versions = _avif.codec_versions()
|
||||||
|
for version in versions.split(", "):
|
||||||
|
if version.split(" [")[0] == codec_name:
|
||||||
|
return version.split(":")[-1].split(" ")[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _accept(prefix: bytes) -> bool | str:
|
||||||
|
if prefix[4:8] != b"ftyp":
|
||||||
|
return False
|
||||||
|
major_brand = prefix[8:12]
|
||||||
|
if major_brand in (
|
||||||
|
# coding brands
|
||||||
|
b"avif",
|
||||||
|
b"avis",
|
||||||
|
# We accept files with AVIF container brands; we can't yet know if
|
||||||
|
# the ftyp box has the correct compatible brands, but if it doesn't
|
||||||
|
# then the plugin will raise a SyntaxError which Pillow will catch
|
||||||
|
# before moving on to the next plugin that accepts the file.
|
||||||
|
#
|
||||||
|
# Also, because this file might not actually be an AVIF file, we
|
||||||
|
# don't raise an error if AVIF support isn't properly compiled.
|
||||||
|
b"mif1",
|
||||||
|
b"msf1",
|
||||||
|
):
|
||||||
|
if not SUPPORTED:
|
||||||
|
return (
|
||||||
|
"image file could not be identified because AVIF support not installed"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_max_threads() -> int:
|
||||||
|
if DEFAULT_MAX_THREADS:
|
||||||
|
return DEFAULT_MAX_THREADS
|
||||||
|
if hasattr(os, "sched_getaffinity"):
|
||||||
|
return len(os.sched_getaffinity(0))
|
||||||
|
else:
|
||||||
|
return os.cpu_count() or 1
|
||||||
|
|
||||||
|
|
||||||
|
class AvifImageFile(ImageFile.ImageFile):
|
||||||
|
format = "AVIF"
|
||||||
|
format_description = "AVIF image"
|
||||||
|
__frame = -1
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
if not SUPPORTED:
|
||||||
|
msg = "image file could not be opened because AVIF support not installed"
|
||||||
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
|
||||||
|
DECODE_CODEC_CHOICE
|
||||||
|
):
|
||||||
|
msg = "Invalid opening codec"
|
||||||
|
raise ValueError(msg)
|
||||||
|
self._decoder = _avif.AvifDecoder(
|
||||||
|
self.fp.read(),
|
||||||
|
DECODE_CODEC_CHOICE,
|
||||||
|
_get_default_max_threads(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get info from decoder
|
||||||
|
self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = (
|
||||||
|
self._decoder.get_info()
|
||||||
|
)
|
||||||
|
self.is_animated = self.n_frames > 1
|
||||||
|
|
||||||
|
if icc:
|
||||||
|
self.info["icc_profile"] = icc
|
||||||
|
if xmp:
|
||||||
|
self.info["xmp"] = xmp
|
||||||
|
|
||||||
|
if exif_orientation != 1 or exif:
|
||||||
|
exif_data = Image.Exif()
|
||||||
|
if exif:
|
||||||
|
exif_data.load(exif)
|
||||||
|
original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
|
||||||
|
else:
|
||||||
|
original_orientation = 1
|
||||||
|
if exif_orientation != original_orientation:
|
||||||
|
exif_data[ExifTags.Base.Orientation] = exif_orientation
|
||||||
|
exif = exif_data.tobytes()
|
||||||
|
if exif:
|
||||||
|
self.info["exif"] = exif
|
||||||
|
self.seek(0)
|
||||||
|
|
||||||
|
def seek(self, frame: int) -> None:
|
||||||
|
if not self._seek_check(frame):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set tile
|
||||||
|
self.__frame = frame
|
||||||
|
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
|
||||||
|
|
||||||
|
def load(self) -> Image.core.PixelAccess | None:
|
||||||
|
if self.tile:
|
||||||
|
# We need to load the image data for this frame
|
||||||
|
data, timescale, pts_in_timescales, duration_in_timescales = (
|
||||||
|
self._decoder.get_frame(self.__frame)
|
||||||
|
)
|
||||||
|
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
|
||||||
|
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
|
||||||
|
|
||||||
|
if self.fp and self._exclusive_fp:
|
||||||
|
self.fp.close()
|
||||||
|
self.fp = BytesIO(data)
|
||||||
|
|
||||||
|
return super().load()
|
||||||
|
|
||||||
|
def load_seek(self, pos: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tell(self) -> int:
|
||||||
|
return self.__frame
|
||||||
|
|
||||||
|
|
||||||
|
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
_save(im, fp, filename, save_all=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _save(
|
||||||
|
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
|
||||||
|
) -> None:
|
||||||
|
info = im.encoderinfo.copy()
|
||||||
|
if save_all:
|
||||||
|
append_images = list(info.get("append_images", []))
|
||||||
|
else:
|
||||||
|
append_images = []
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for ims in [im] + append_images:
|
||||||
|
total += getattr(ims, "n_frames", 1)
|
||||||
|
|
||||||
|
quality = info.get("quality", 75)
|
||||||
|
if not isinstance(quality, int) or quality < 0 or quality > 100:
|
||||||
|
msg = "Invalid quality setting"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
duration = info.get("duration", 0)
|
||||||
|
subsampling = info.get("subsampling", "4:2:0")
|
||||||
|
speed = info.get("speed", 6)
|
||||||
|
max_threads = info.get("max_threads", _get_default_max_threads())
|
||||||
|
codec = info.get("codec", "auto")
|
||||||
|
if codec != "auto" and not _avif.encoder_codec_available(codec):
|
||||||
|
msg = "Invalid saving codec"
|
||||||
|
raise ValueError(msg)
|
||||||
|
range_ = info.get("range", "full")
|
||||||
|
tile_rows_log2 = info.get("tile_rows", 0)
|
||||||
|
tile_cols_log2 = info.get("tile_cols", 0)
|
||||||
|
alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
|
||||||
|
autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
|
||||||
|
|
||||||
|
icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
|
||||||
|
exif_orientation = 1
|
||||||
|
if exif := info.get("exif"):
|
||||||
|
if isinstance(exif, Image.Exif):
|
||||||
|
exif_data = exif
|
||||||
|
else:
|
||||||
|
exif_data = Image.Exif()
|
||||||
|
exif_data.load(exif)
|
||||||
|
if ExifTags.Base.Orientation in exif_data:
|
||||||
|
exif_orientation = exif_data.pop(ExifTags.Base.Orientation)
|
||||||
|
exif = exif_data.tobytes() if exif_data else b""
|
||||||
|
elif isinstance(exif, Image.Exif):
|
||||||
|
exif = exif_data.tobytes()
|
||||||
|
|
||||||
|
xmp = info.get("xmp")
|
||||||
|
|
||||||
|
if isinstance(xmp, str):
|
||||||
|
xmp = xmp.encode("utf-8")
|
||||||
|
|
||||||
|
advanced = info.get("advanced")
|
||||||
|
if advanced is not None:
|
||||||
|
if isinstance(advanced, dict):
|
||||||
|
advanced = advanced.items()
|
||||||
|
try:
|
||||||
|
advanced = tuple(advanced)
|
||||||
|
except TypeError:
|
||||||
|
invalid = True
|
||||||
|
else:
|
||||||
|
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
|
||||||
|
if invalid:
|
||||||
|
msg = (
|
||||||
|
"advanced codec options must be a dict of key-value string "
|
||||||
|
"pairs or a series of key-value two-tuples"
|
||||||
|
)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# Setup the AVIF encoder
|
||||||
|
enc = _avif.AvifEncoder(
|
||||||
|
im.size,
|
||||||
|
subsampling,
|
||||||
|
quality,
|
||||||
|
speed,
|
||||||
|
max_threads,
|
||||||
|
codec,
|
||||||
|
range_,
|
||||||
|
tile_rows_log2,
|
||||||
|
tile_cols_log2,
|
||||||
|
alpha_premultiplied,
|
||||||
|
autotiling,
|
||||||
|
icc_profile or b"",
|
||||||
|
exif or b"",
|
||||||
|
exif_orientation,
|
||||||
|
xmp or b"",
|
||||||
|
advanced,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add each frame
|
||||||
|
frame_idx = 0
|
||||||
|
frame_duration = 0
|
||||||
|
cur_idx = im.tell()
|
||||||
|
is_single_frame = total == 1
|
||||||
|
try:
|
||||||
|
for ims in [im] + append_images:
|
||||||
|
# Get number of frames in this image
|
||||||
|
nfr = getattr(ims, "n_frames", 1)
|
||||||
|
|
||||||
|
for idx in range(nfr):
|
||||||
|
ims.seek(idx)
|
||||||
|
|
||||||
|
# Make sure image mode is supported
|
||||||
|
frame = ims
|
||||||
|
rawmode = ims.mode
|
||||||
|
if ims.mode not in {"RGB", "RGBA"}:
|
||||||
|
rawmode = "RGBA" if ims.has_transparency_data else "RGB"
|
||||||
|
frame = ims.convert(rawmode)
|
||||||
|
|
||||||
|
# Update frame duration
|
||||||
|
if isinstance(duration, (list, tuple)):
|
||||||
|
frame_duration = duration[frame_idx]
|
||||||
|
else:
|
||||||
|
frame_duration = duration
|
||||||
|
|
||||||
|
# Append the frame to the animation encoder
|
||||||
|
enc.add(
|
||||||
|
frame.tobytes("raw", rawmode),
|
||||||
|
frame_duration,
|
||||||
|
frame.size,
|
||||||
|
rawmode,
|
||||||
|
is_single_frame,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update frame index
|
||||||
|
frame_idx += 1
|
||||||
|
|
||||||
|
if not save_all:
|
||||||
|
break
|
||||||
|
|
||||||
|
finally:
|
||||||
|
im.seek(cur_idx)
|
||||||
|
|
||||||
|
# Get the final output from the encoder
|
||||||
|
data = enc.finish()
|
||||||
|
if data is None:
|
||||||
|
msg = "cannot write file as AVIF (encoder returned None)"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
|
fp.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
|
||||||
|
if SUPPORTED:
|
||||||
|
Image.register_save(AvifImageFile.format, _save)
|
||||||
|
Image.register_save_all(AvifImageFile.format, _save_all)
|
||||||
|
Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
|
||||||
|
Image.register_mime(AvifImageFile.format, "image/avif")
|
|
@ -31,7 +31,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union
|
from typing import IO, Any, Literal, NamedTuple, Union
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
Image,
|
Image,
|
||||||
|
@ -47,6 +47,7 @@ from ._binary import o8
|
||||||
from ._binary import o16le as o16
|
from ._binary import o16le as o16
|
||||||
from ._util import DeferredError
|
from ._util import DeferredError
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import _imaging
|
from . import _imaging
|
||||||
from ._typing import Buffer
|
from ._typing import Buffer
|
||||||
|
|
|
@ -41,14 +41,7 @@ import warnings
|
||||||
from collections.abc import Callable, Iterator, MutableMapping, Sequence
|
from collections.abc import Callable, Iterator, MutableMapping, Sequence
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import (
|
from typing import IO, Any, Literal, Protocol, cast
|
||||||
IO,
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
Literal,
|
|
||||||
Protocol,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
# VERSION was removed in Pillow 6.0.0.
|
# VERSION was removed in Pillow 6.0.0.
|
||||||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||||
|
@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Registries
|
# Registries
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import mmap
|
import mmap
|
||||||
from xml.etree.ElementTree import Element
|
from xml.etree.ElementTree import Element
|
||||||
|
@ -583,6 +577,14 @@ class Image:
|
||||||
def mode(self) -> str:
|
def mode(self) -> str:
|
||||||
return self._mode
|
return self._mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def readonly(self) -> int:
|
||||||
|
return (self._im and self._im.readonly) or self._readonly
|
||||||
|
|
||||||
|
@readonly.setter
|
||||||
|
def readonly(self, readonly: int) -> None:
|
||||||
|
self._readonly = readonly
|
||||||
|
|
||||||
def _new(self, im: core.ImagingCore) -> Image:
|
def _new(self, im: core.ImagingCore) -> Image:
|
||||||
new = Image()
|
new = Image()
|
||||||
new.im = im
|
new.im = im
|
||||||
|
@ -734,6 +736,16 @@ class Image:
|
||||||
new["shape"], new["typestr"] = _conv_type_shape(self)
|
new["shape"], new["typestr"] = _conv_type_shape(self)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __arrow_c_schema__(self) -> object:
|
||||||
|
self.load()
|
||||||
|
return self.im.__arrow_c_schema__()
|
||||||
|
|
||||||
|
def __arrow_c_array__(
|
||||||
|
self, requested_schema: object | None = None
|
||||||
|
) -> tuple[object, object]:
|
||||||
|
self.load()
|
||||||
|
return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__())
|
||||||
|
|
||||||
def __getstate__(self) -> list[Any]:
|
def __getstate__(self) -> list[Any]:
|
||||||
im_data = self.tobytes() # load image first
|
im_data = self.tobytes() # load image first
|
||||||
return [self.info, self.mode, self.size, self.getpalette(), im_data]
|
return [self.info, self.mode, self.size, self.getpalette(), im_data]
|
||||||
|
@ -1526,6 +1538,8 @@ class Image:
|
||||||
# XMP tags
|
# XMP tags
|
||||||
if ExifTags.Base.Orientation not in self._exif:
|
if ExifTags.Base.Orientation not in self._exif:
|
||||||
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
xmp_tags = self.info.get("XML:com.adobe.xmp")
|
||||||
|
if not xmp_tags and (xmp_tags := self.info.get("xmp")):
|
||||||
|
xmp_tags = xmp_tags.decode("utf-8")
|
||||||
if xmp_tags:
|
if xmp_tags:
|
||||||
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
|
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
|
||||||
if match:
|
if match:
|
||||||
|
@ -3205,6 +3219,18 @@ class SupportsArrayInterface(Protocol):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class SupportsArrowArrayInterface(Protocol):
|
||||||
|
"""
|
||||||
|
An object that has an ``__arrow_c_array__`` method corresponding to the arrow c
|
||||||
|
data interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __arrow_c_array__(
|
||||||
|
self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037
|
||||||
|
) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
|
def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
|
||||||
"""
|
"""
|
||||||
Creates an image memory from an object exporting the array interface
|
Creates an image memory from an object exporting the array interface
|
||||||
|
@ -3293,6 +3319,56 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
|
||||||
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
|
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def fromarrow(obj: SupportsArrowArrayInterface, mode, size) -> Image:
|
||||||
|
"""Creates an image with zero-copy shared memory from an object exporting
|
||||||
|
the arrow_c_array interface protocol::
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
import pyarrow as pa
|
||||||
|
arr = pa.array([0]*(5*5*4), type=pa.uint8())
|
||||||
|
im = Image.fromarrow(arr, 'RGBA', (5, 5))
|
||||||
|
|
||||||
|
If the data representation of the ``obj`` is not compatible with
|
||||||
|
Pillow internal storage, a ValueError is raised.
|
||||||
|
|
||||||
|
Pillow images can also be converted to Arrow objects::
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
import pyarrow as pa
|
||||||
|
im = Image.open('hopper.jpg')
|
||||||
|
arr = pa.array(im)
|
||||||
|
|
||||||
|
As with array support, when converting Pillow images to arrays,
|
||||||
|
only pixel values are transferred. This means that P and PA mode
|
||||||
|
images will lose their palette.
|
||||||
|
|
||||||
|
:param obj: Object with an arrow_c_array interface
|
||||||
|
:param mode: Image mode.
|
||||||
|
:param size: Image size. This must match the storage of the arrow object.
|
||||||
|
:returns: An Image object
|
||||||
|
|
||||||
|
Note that according to the Arrow spec, both the producer and the
|
||||||
|
consumer should consider the exported array to be immutable, as
|
||||||
|
unsynchronized updates will potentially cause inconsistent data.
|
||||||
|
|
||||||
|
See: :ref:`arrow-support` for more detailed information
|
||||||
|
|
||||||
|
.. versionadded:: 11.2.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not hasattr(obj, "__arrow_c_array__"):
|
||||||
|
msg = "arrow_c_array interface not found"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
(schema_capsule, array_capsule) = obj.__arrow_c_array__()
|
||||||
|
_im = core.new_arrow(mode, size, schema_capsule, array_capsule)
|
||||||
|
if _im:
|
||||||
|
return Image()._new(_im)
|
||||||
|
|
||||||
|
msg = "new_arrow returned None without an exception"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile:
|
def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile:
|
||||||
"""Creates an image instance from a QImage image"""
|
"""Creates an image instance from a QImage image"""
|
||||||
from . import ImageQt
|
from . import ImageQt
|
||||||
|
|
|
@ -35,7 +35,7 @@ import math
|
||||||
import struct
|
import struct
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast
|
from typing import Any, AnyStr, Callable, Union, cast
|
||||||
|
|
||||||
from . import Image, ImageColor
|
from . import Image, ImageColor
|
||||||
from ._deprecate import deprecate
|
from ._deprecate import deprecate
|
||||||
|
@ -44,6 +44,7 @@ from ._typing import Coords
|
||||||
# experimental access to the outline API
|
# experimental access to the outline API
|
||||||
Outline: Callable[[], Image.core._Outline] = Image.core.outline
|
Outline: Callable[[], Image.core._Outline] = Image.core.outline
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import ImageDraw2, ImageFont
|
from . import ImageDraw2, ImageFont
|
||||||
|
|
||||||
|
|
|
@ -34,12 +34,13 @@ import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
|
from typing import IO, Any, NamedTuple, cast
|
||||||
|
|
||||||
from . import ExifTags, Image
|
from . import ExifTags, Image
|
||||||
from ._deprecate import deprecate
|
from ._deprecate import deprecate
|
||||||
from ._util import DeferredError, is_path
|
from ._util import DeferredError, is_path
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._typing import StrOrBytesPath
|
from ._typing import StrOrBytesPath
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,9 @@ import abc
|
||||||
import functools
|
import functools
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import TYPE_CHECKING, Any, Callable, cast
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import _imaging
|
from . import _imaging
|
||||||
from ._typing import NumpyArray
|
from ._typing import NumpyArray
|
||||||
|
|
|
@ -34,12 +34,13 @@ import warnings
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast
|
from typing import IO, Any, BinaryIO, TypedDict, cast
|
||||||
|
|
||||||
from . import Image, features
|
from . import Image, features
|
||||||
from ._typing import StrOrBytesPath
|
from ._typing import StrOrBytesPath
|
||||||
from ._util import DeferredError, is_path
|
from ._util import DeferredError, is_path
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import ImageFile
|
from . import ImageFile
|
||||||
from ._imaging import ImagingFont
|
from ._imaging import ImagingFont
|
||||||
|
|
|
@ -19,10 +19,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import array
|
import array
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import IO, TYPE_CHECKING
|
from typing import IO
|
||||||
|
|
||||||
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import Image
|
from . import Image
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Union
|
from typing import Any, Callable, Union
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
from ._util import is_path
|
from ._util import is_path
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import PyQt6
|
import PyQt6
|
||||||
import PySide6
|
import PySide6
|
||||||
|
|
|
@ -28,10 +28,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import tkinter
|
import tkinter
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import Any
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._typing import CapsuleType
|
from ._typing import CapsuleType
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
from typing import IO, TYPE_CHECKING, Any
|
from typing import IO, Any
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
@ -52,6 +52,7 @@ from ._binary import o16be as o16
|
||||||
from ._deprecate import deprecate
|
from ._deprecate import deprecate
|
||||||
from .JpegPresets import presets
|
from .JpegPresets import presets
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .MpoImagePlugin import MpoImageFile
|
from .MpoImagePlugin import MpoImageFile
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from typing import IO, TYPE_CHECKING
|
from typing import IO
|
||||||
|
|
||||||
from . import EpsImagePlugin
|
from . import EpsImagePlugin
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Simple PostScript graphics interface.
|
# Simple PostScript graphics interface.
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import zlib
|
import zlib
|
||||||
from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union
|
from typing import IO, Any, NamedTuple, Union
|
||||||
|
|
||||||
|
|
||||||
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
|
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
|
||||||
|
@ -251,6 +251,7 @@ class PdfArray(list[Any]):
|
||||||
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
|
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
|
||||||
|
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_DictBase = collections.UserDict[Union[str, bytes], Any]
|
_DictBase = collections.UserDict[Union[str, bytes], Any]
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -40,7 +40,7 @@ import warnings
|
||||||
import zlib
|
import zlib
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast
|
from typing import IO, Any, NamedTuple, NoReturn, cast
|
||||||
|
|
||||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
@ -50,6 +50,7 @@ from ._binary import o16be as o16
|
||||||
from ._binary import o32be as o32
|
from ._binary import o32be as o32
|
||||||
from ._util import DeferredError
|
from ._util import DeferredError
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import _imaging
|
from . import _imaging
|
||||||
|
|
||||||
|
|
|
@ -37,11 +37,13 @@ from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
from typing import IO, TYPE_CHECKING, Any, cast
|
from typing import IO, Any, cast
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
from ._util import DeferredError
|
from ._util import DeferredError
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
|
||||||
|
|
||||||
def isInt(f: Any) -> int:
|
def isInt(f: Any) -> int:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
|
||||||
while True:
|
while True:
|
||||||
s = self.fh.read(512)
|
s = self.fh.read(512)
|
||||||
if len(s) != 512:
|
if len(s) != 512:
|
||||||
|
self.fh.close()
|
||||||
|
|
||||||
msg = "unexpected end of tar file"
|
msg = "unexpected end of tar file"
|
||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
|
|
||||||
name = s[:100].decode("utf-8")
|
name = s[:100].decode("utf-8")
|
||||||
i = name.find("\0")
|
i = name.find("\0")
|
||||||
if i == 0:
|
if i == 0:
|
||||||
|
self.fh.close()
|
||||||
|
|
||||||
msg = "cannot find subfile"
|
msg = "cannot find subfile"
|
||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
if i > 0:
|
if i > 0:
|
||||||
|
|
|
@ -50,7 +50,7 @@ import warnings
|
||||||
from collections.abc import Iterator, MutableMapping
|
from collections.abc import Iterator, MutableMapping
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from numbers import Number, Rational
|
from numbers import Number, Rational
|
||||||
from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast
|
from typing import IO, Any, Callable, NoReturn, cast
|
||||||
|
|
||||||
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
@ -61,6 +61,7 @@ from ._typing import StrOrBytesPath
|
||||||
from ._util import DeferredError, is_path
|
from ._util import DeferredError, is_path
|
||||||
from .TiffTags import TYPES
|
from .TiffTags import TYPES
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._typing import Buffer, IntegralLike
|
from ._typing import Buffer, IntegralLike
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ del _version
|
||||||
|
|
||||||
|
|
||||||
_plugins = [
|
_plugins = [
|
||||||
|
"AvifImagePlugin",
|
||||||
"BlpImagePlugin",
|
"BlpImagePlugin",
|
||||||
"BmpImagePlugin",
|
"BmpImagePlugin",
|
||||||
"BufrStubImagePlugin",
|
"BufrStubImagePlugin",
|
||||||
|
|
3
src/PIL/_avif.pyi
Normal file
3
src/PIL/_avif.pyi
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> Any: ...
|
|
@ -3,8 +3,9 @@ from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union
|
from typing import Any, Protocol, TypeVar, Union
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from numbers import _IntegralLike as IntegralLike
|
from numbers import _IntegralLike as IntegralLike
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ modules = {
|
||||||
"freetype2": ("PIL._imagingft", "freetype2_version"),
|
"freetype2": ("PIL._imagingft", "freetype2_version"),
|
||||||
"littlecms2": ("PIL._imagingcms", "littlecms_version"),
|
"littlecms2": ("PIL._imagingcms", "littlecms_version"),
|
||||||
"webp": ("PIL._webp", "webpdecoder_version"),
|
"webp": ("PIL._webp", "webpdecoder_version"),
|
||||||
|
"avif": ("PIL._avif", "libavif_version"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -288,6 +289,7 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
|
||||||
("freetype2", "FREETYPE2"),
|
("freetype2", "FREETYPE2"),
|
||||||
("littlecms2", "LITTLECMS2"),
|
("littlecms2", "LITTLECMS2"),
|
||||||
("webp", "WEBP"),
|
("webp", "WEBP"),
|
||||||
|
("avif", "AVIF"),
|
||||||
("jpg", "JPEG"),
|
("jpg", "JPEG"),
|
||||||
("jpg_2000", "OPENJPEG (JPEG2000)"),
|
("jpg_2000", "OPENJPEG (JPEG2000)"),
|
||||||
("zlib", "ZLIB (PNG/ZIP)"),
|
("zlib", "ZLIB (PNG/ZIP)"),
|
||||||
|
|
908
src/_avif.c
Normal file
908
src/_avif.c
Normal file
|
@ -0,0 +1,908 @@
|
||||||
|
#define PY_SSIZE_T_CLEAN
|
||||||
|
|
||||||
|
#include <Python.h>
|
||||||
|
#include "avif/avif.h"
|
||||||
|
|
||||||
|
// Encoder type
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD avifEncoder *encoder;
|
||||||
|
avifImage *image;
|
||||||
|
int first_frame;
|
||||||
|
} AvifEncoderObject;
|
||||||
|
|
||||||
|
static PyTypeObject AvifEncoder_Type;
|
||||||
|
|
||||||
|
// Decoder type
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD avifDecoder *decoder;
|
||||||
|
Py_buffer buffer;
|
||||||
|
} AvifDecoderObject;
|
||||||
|
|
||||||
|
static PyTypeObject AvifDecoder_Type;
|
||||||
|
|
||||||
|
static int
|
||||||
|
normalize_tiles_log2(int value) {
|
||||||
|
if (value < 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (value > 6) {
|
||||||
|
return 6;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
exc_type_for_avif_result(avifResult result) {
|
||||||
|
switch (result) {
|
||||||
|
case AVIF_RESULT_INVALID_EXIF_PAYLOAD:
|
||||||
|
case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION:
|
||||||
|
return PyExc_ValueError;
|
||||||
|
case AVIF_RESULT_INVALID_FTYP:
|
||||||
|
case AVIF_RESULT_BMFF_PARSE_FAILED:
|
||||||
|
case AVIF_RESULT_TRUNCATED_DATA:
|
||||||
|
case AVIF_RESULT_NO_CONTENT:
|
||||||
|
return PyExc_SyntaxError;
|
||||||
|
default:
|
||||||
|
return PyExc_RuntimeError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t
|
||||||
|
irot_imir_to_exif_orientation(const avifImage *image) {
|
||||||
|
uint8_t axis = image->imir.axis;
|
||||||
|
int imir = image->transformFlags & AVIF_TRANSFORM_IMIR;
|
||||||
|
int irot = image->transformFlags & AVIF_TRANSFORM_IROT;
|
||||||
|
if (irot) {
|
||||||
|
uint8_t angle = image->irot.angle;
|
||||||
|
if (angle == 1) {
|
||||||
|
if (imir) {
|
||||||
|
return axis ? 7 // 90 degrees anti-clockwise then swap left and right.
|
||||||
|
: 5; // 90 degrees anti-clockwise then swap top and bottom.
|
||||||
|
}
|
||||||
|
return 6; // 90 degrees anti-clockwise.
|
||||||
|
}
|
||||||
|
if (angle == 2) {
|
||||||
|
if (imir) {
|
||||||
|
return axis
|
||||||
|
? 4 // 180 degrees anti-clockwise then swap left and right.
|
||||||
|
: 2; // 180 degrees anti-clockwise then swap top and bottom.
|
||||||
|
}
|
||||||
|
return 3; // 180 degrees anti-clockwise.
|
||||||
|
}
|
||||||
|
if (angle == 3) {
|
||||||
|
if (imir) {
|
||||||
|
return axis
|
||||||
|
? 5 // 270 degrees anti-clockwise then swap left and right.
|
||||||
|
: 7; // 270 degrees anti-clockwise then swap top and bottom.
|
||||||
|
}
|
||||||
|
return 8; // 270 degrees anti-clockwise.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imir) {
|
||||||
|
return axis ? 2 // Swap left and right.
|
||||||
|
: 4; // Swap top and bottom.
|
||||||
|
}
|
||||||
|
return 1; // Default orientation ("top-left", no-op).
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
exif_orientation_to_irot_imir(avifImage *image, int orientation) {
|
||||||
|
// Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A
|
||||||
|
// Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021
|
||||||
|
// sections 6.5.10 and 6.5.12.
|
||||||
|
switch (orientation) {
|
||||||
|
case 2: // The 0th row is at the visual top of the image, and the 0th column is
|
||||||
|
// the visual right-hand side.
|
||||||
|
image->transformFlags |= AVIF_TRANSFORM_IMIR;
|
||||||
|
image->imir.axis = 1;
|
||||||
|
break;
|
||||||
|
case 3: // The 0th row is at the visual bottom of the image, and the 0th column
|
||||||
|
// is the visual right-hand side.
|
||||||
|
image->transformFlags |= AVIF_TRANSFORM_IROT;
|
||||||
|
image->irot.angle = 2;
|
||||||
|
break;
|
||||||
|
case 4: // The 0th row is at the visual bottom of the image, and the 0th column
|
||||||
|
// is the visual left-hand side.
|
||||||
|
image->transformFlags |= AVIF_TRANSFORM_IMIR;
|
||||||
|
break;
|
||||||
|
case 5: // The 0th row is the visual left-hand side of the image, and the 0th
|
||||||
|
// column is the visual top.
|
||||||
|
image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
|
||||||
|
image->irot.angle = 1; // applied before imir according to MIAF spec
|
||||||
|
// ISO/IEC 28002-12:2021 - section 7.3.6.7
|
||||||
|
break;
|
||||||
|
case 6: // The 0th row is the visual right-hand side of the image, and the 0th
|
||||||
|
// column is the visual top.
|
||||||
|
image->transformFlags |= AVIF_TRANSFORM_IROT;
|
||||||
|
image->irot.angle = 3;
|
||||||
|
break;
|
||||||
|
case 7: // The 0th row is the visual right-hand side of the image, and the 0th
|
||||||
|
// column is the visual bottom.
|
||||||
|
image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
|
||||||
|
image->irot.angle = 3; // applied before imir according to MIAF spec
|
||||||
|
// ISO/IEC 28002-12:2021 - section 7.3.6.7
|
||||||
|
break;
|
||||||
|
case 8: // The 0th row is the visual left-hand side of the image, and the 0th
|
||||||
|
// column is the visual bottom.
|
||||||
|
image->transformFlags |= AVIF_TRANSFORM_IROT;
|
||||||
|
image->irot.angle = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
_codec_available(const char *name, avifCodecFlags flags) {
|
||||||
|
avifCodecChoice codec = avifCodecChoiceFromName(name);
|
||||||
|
if (codec == AVIF_CODEC_CHOICE_AUTO) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const char *codec_name = avifCodecName(codec, flags);
|
||||||
|
return (codec_name == NULL) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_decoder_codec_available(PyObject *self, PyObject *args) {
|
||||||
|
char *codec_name;
|
||||||
|
if (!PyArg_ParseTuple(args, "s", &codec_name)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE);
|
||||||
|
return PyBool_FromLong(is_available);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_encoder_codec_available(PyObject *self, PyObject *args) {
|
||||||
|
char *codec_name;
|
||||||
|
if (!PyArg_ParseTuple(args, "s", &codec_name)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE);
|
||||||
|
return PyBool_FromLong(is_available);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_codec_versions(PyObject *self, PyObject *args) {
|
||||||
|
char buffer[256];
|
||||||
|
avifCodecVersions(buffer);
|
||||||
|
return PyUnicode_FromString(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
_add_codec_specific_options(avifEncoder *encoder, PyObject *opts) {
|
||||||
|
Py_ssize_t i, size;
|
||||||
|
PyObject *keyval, *py_key, *py_val;
|
||||||
|
if (!PyTuple_Check(opts)) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
size = PyTuple_GET_SIZE(opts);
|
||||||
|
|
||||||
|
for (i = 0; i < size; i++) {
|
||||||
|
keyval = PyTuple_GetItem(opts, i);
|
||||||
|
if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
py_key = PyTuple_GetItem(keyval, 0);
|
||||||
|
py_val = PyTuple_GetItem(keyval, 1);
|
||||||
|
if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const char *key = PyUnicode_AsUTF8(py_key);
|
||||||
|
const char *val = PyUnicode_AsUTF8(py_val);
|
||||||
|
if (key == NULL || val == NULL) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Setting advanced codec options failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoder functions
|
||||||
|
PyObject *
|
||||||
|
AvifEncoderNew(PyObject *self_, PyObject *args) {
|
||||||
|
unsigned int width, height;
|
||||||
|
AvifEncoderObject *self = NULL;
|
||||||
|
avifEncoder *encoder = NULL;
|
||||||
|
|
||||||
|
char *subsampling;
|
||||||
|
int quality;
|
||||||
|
int speed;
|
||||||
|
int exif_orientation;
|
||||||
|
int max_threads;
|
||||||
|
Py_buffer icc_buffer;
|
||||||
|
Py_buffer exif_buffer;
|
||||||
|
Py_buffer xmp_buffer;
|
||||||
|
int alpha_premultiplied;
|
||||||
|
int autotiling;
|
||||||
|
int tile_rows_log2;
|
||||||
|
int tile_cols_log2;
|
||||||
|
|
||||||
|
char *codec;
|
||||||
|
char *range;
|
||||||
|
|
||||||
|
PyObject *advanced;
|
||||||
|
int error = 0;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(
|
||||||
|
args,
|
||||||
|
"(II)siiissiippy*y*iy*O",
|
||||||
|
&width,
|
||||||
|
&height,
|
||||||
|
&subsampling,
|
||||||
|
&quality,
|
||||||
|
&speed,
|
||||||
|
&max_threads,
|
||||||
|
&codec,
|
||||||
|
&range,
|
||||||
|
&tile_rows_log2,
|
||||||
|
&tile_cols_log2,
|
||||||
|
&alpha_premultiplied,
|
||||||
|
&autotiling,
|
||||||
|
&icc_buffer,
|
||||||
|
&exif_buffer,
|
||||||
|
&exif_orientation,
|
||||||
|
&xmp_buffer,
|
||||||
|
&advanced
|
||||||
|
)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new animation encoder and picture frame
|
||||||
|
avifImage *image = avifImageCreateEmpty();
|
||||||
|
if (image == NULL) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Image creation failed");
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set these in advance so any upcoming RGB -> YUV use the proper coefficients
|
||||||
|
if (strcmp(range, "full") == 0) {
|
||||||
|
image->yuvRange = AVIF_RANGE_FULL;
|
||||||
|
} else if (strcmp(range, "limited") == 0) {
|
||||||
|
image->yuvRange = AVIF_RANGE_LIMITED;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Invalid range");
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
if (strcmp(subsampling, "4:0:0") == 0) {
|
||||||
|
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
|
||||||
|
} else if (strcmp(subsampling, "4:2:0") == 0) {
|
||||||
|
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420;
|
||||||
|
} else if (strcmp(subsampling, "4:2:2") == 0) {
|
||||||
|
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422;
|
||||||
|
} else if (strcmp(subsampling, "4:4:4") == 0) {
|
||||||
|
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
|
||||||
|
} else {
|
||||||
|
PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate canvas dimensions
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions");
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
image->width = width;
|
||||||
|
image->height = height;
|
||||||
|
|
||||||
|
image->depth = 8;
|
||||||
|
image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE;
|
||||||
|
|
||||||
|
encoder = avifEncoderCreate();
|
||||||
|
if (!encoder) {
|
||||||
|
PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder");
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
int is_aom_encode = strcmp(codec, "aom") == 0 ||
|
||||||
|
(strcmp(codec, "auto") == 0 &&
|
||||||
|
_codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE));
|
||||||
|
encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads;
|
||||||
|
|
||||||
|
encoder->quality = quality;
|
||||||
|
|
||||||
|
if (strcmp(codec, "auto") == 0) {
|
||||||
|
encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO;
|
||||||
|
} else {
|
||||||
|
encoder->codecChoice = avifCodecChoiceFromName(codec);
|
||||||
|
}
|
||||||
|
if (speed < AVIF_SPEED_SLOWEST) {
|
||||||
|
speed = AVIF_SPEED_SLOWEST;
|
||||||
|
} else if (speed > AVIF_SPEED_FASTEST) {
|
||||||
|
speed = AVIF_SPEED_FASTEST;
|
||||||
|
}
|
||||||
|
encoder->speed = speed;
|
||||||
|
encoder->timescale = (uint64_t)1000;
|
||||||
|
|
||||||
|
encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE;
|
||||||
|
if (!autotiling) {
|
||||||
|
encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2);
|
||||||
|
encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) {
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type);
|
||||||
|
if (!self) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "could not create encoder object");
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
self->first_frame = 1;
|
||||||
|
|
||||||
|
avifResult result;
|
||||||
|
if (icc_buffer.len) {
|
||||||
|
result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Setting ICC profile failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
// colorPrimaries and transferCharacteristics are ignored when an ICC
|
||||||
|
// profile is present, so set them to UNSPECIFIED.
|
||||||
|
image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED;
|
||||||
|
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED;
|
||||||
|
} else {
|
||||||
|
image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709;
|
||||||
|
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
|
||||||
|
}
|
||||||
|
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
|
||||||
|
|
||||||
|
if (exif_buffer.len) {
|
||||||
|
result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Setting EXIF data failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xmp_buffer.len) {
|
||||||
|
result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Setting XMP data failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exif_orientation > 1) {
|
||||||
|
exif_orientation_to_irot_imir(image, exif_orientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
self->image = image;
|
||||||
|
self->encoder = encoder;
|
||||||
|
|
||||||
|
end:
|
||||||
|
PyBuffer_Release(&icc_buffer);
|
||||||
|
PyBuffer_Release(&exif_buffer);
|
||||||
|
PyBuffer_Release(&xmp_buffer);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (image) {
|
||||||
|
avifImageDestroy(image);
|
||||||
|
}
|
||||||
|
if (encoder) {
|
||||||
|
avifEncoderDestroy(encoder);
|
||||||
|
}
|
||||||
|
if (self) {
|
||||||
|
PyObject_Del(self);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (PyObject *)self;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_encoder_dealloc(AvifEncoderObject *self) {
|
||||||
|
if (self->encoder) {
|
||||||
|
avifEncoderDestroy(self->encoder);
|
||||||
|
}
|
||||||
|
if (self->image) {
|
||||||
|
avifImageDestroy(self->image);
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_encoder_add(AvifEncoderObject *self, PyObject *args) {
|
||||||
|
uint8_t *rgb_bytes;
|
||||||
|
Py_ssize_t size;
|
||||||
|
unsigned int duration;
|
||||||
|
unsigned int width;
|
||||||
|
unsigned int height;
|
||||||
|
char *mode;
|
||||||
|
unsigned int is_single_frame;
|
||||||
|
int error = 0;
|
||||||
|
|
||||||
|
avifRGBImage rgb;
|
||||||
|
avifResult result;
|
||||||
|
|
||||||
|
avifEncoder *encoder = self->encoder;
|
||||||
|
avifImage *image = self->image;
|
||||||
|
avifImage *frame = NULL;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(
|
||||||
|
args,
|
||||||
|
"y#I(II)sp",
|
||||||
|
(char **)&rgb_bytes,
|
||||||
|
&size,
|
||||||
|
&duration,
|
||||||
|
&width,
|
||||||
|
&height,
|
||||||
|
&mode,
|
||||||
|
&is_single_frame
|
||||||
|
)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image->width != width || image->height != height) {
|
||||||
|
PyErr_Format(
|
||||||
|
PyExc_ValueError,
|
||||||
|
"Image sequence dimensions mismatch, %ux%u != %ux%u",
|
||||||
|
image->width,
|
||||||
|
image->height,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->first_frame) {
|
||||||
|
// If we don't have an image populated with yuv planes, this is the first frame
|
||||||
|
frame = image;
|
||||||
|
} else {
|
||||||
|
frame = avifImageCreateEmpty();
|
||||||
|
if (image == NULL) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Image creation failed");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame->width = width;
|
||||||
|
frame->height = height;
|
||||||
|
frame->colorPrimaries = image->colorPrimaries;
|
||||||
|
frame->transferCharacteristics = image->transferCharacteristics;
|
||||||
|
frame->matrixCoefficients = image->matrixCoefficients;
|
||||||
|
frame->yuvRange = image->yuvRange;
|
||||||
|
frame->yuvFormat = image->yuvFormat;
|
||||||
|
frame->depth = image->depth;
|
||||||
|
frame->alphaPremultiplied = image->alphaPremultiplied;
|
||||||
|
}
|
||||||
|
|
||||||
|
avifRGBImageSetDefaults(&rgb, frame);
|
||||||
|
|
||||||
|
if (strcmp(mode, "RGBA") == 0) {
|
||||||
|
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
||||||
|
} else {
|
||||||
|
rgb.format = AVIF_RGB_FORMAT_RGB;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = avifRGBImageAllocatePixels(&rgb);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Pixel allocation failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rgb.rowBytes * rgb.height != size) {
|
||||||
|
PyErr_Format(
|
||||||
|
PyExc_RuntimeError,
|
||||||
|
"rgb data has incorrect size: %u * %u (%u) != %u",
|
||||||
|
rgb.rowBytes,
|
||||||
|
rgb.height,
|
||||||
|
rgb.rowBytes * rgb.height,
|
||||||
|
size
|
||||||
|
);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rgb.pixels is safe for writes
|
||||||
|
memcpy(rgb.pixels, rgb_bytes, size);
|
||||||
|
|
||||||
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
|
result = avifImageRGBToYUV(frame, &rgb);
|
||||||
|
Py_END_ALLOW_THREADS;
|
||||||
|
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Conversion to YUV failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t addImageFlags =
|
||||||
|
is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE;
|
||||||
|
|
||||||
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
|
result = avifEncoderAddImage(encoder, frame, duration, addImageFlags);
|
||||||
|
Py_END_ALLOW_THREADS;
|
||||||
|
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Failed to encode image: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
error = 1;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
end:
|
||||||
|
if (&rgb) {
|
||||||
|
avifRGBImageFreePixels(&rgb);
|
||||||
|
}
|
||||||
|
if (!self->first_frame) {
|
||||||
|
avifImageDestroy(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
self->first_frame = 0;
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_encoder_finish(AvifEncoderObject *self) {
|
||||||
|
avifEncoder *encoder = self->encoder;
|
||||||
|
|
||||||
|
avifRWData raw = AVIF_DATA_EMPTY;
|
||||||
|
avifResult result;
|
||||||
|
PyObject *ret = NULL;
|
||||||
|
|
||||||
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
|
result = avifEncoderFinish(encoder, &raw);
|
||||||
|
Py_END_ALLOW_THREADS;
|
||||||
|
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Failed to finish encoding: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
avifRWDataFree(&raw);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size);
|
||||||
|
|
||||||
|
avifRWDataFree(&raw);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decoder functions
|
||||||
|
PyObject *
|
||||||
|
AvifDecoderNew(PyObject *self_, PyObject *args) {
|
||||||
|
Py_buffer buffer;
|
||||||
|
AvifDecoderObject *self = NULL;
|
||||||
|
avifDecoder *decoder;
|
||||||
|
|
||||||
|
char *codec_str;
|
||||||
|
avifCodecChoice codec;
|
||||||
|
int max_threads;
|
||||||
|
|
||||||
|
avifResult result;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(codec_str, "auto") == 0) {
|
||||||
|
codec = AVIF_CODEC_CHOICE_AUTO;
|
||||||
|
} else {
|
||||||
|
codec = avifCodecChoiceFromName(codec_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type);
|
||||||
|
if (!self) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
|
||||||
|
PyBuffer_Release(&buffer);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder = avifDecoderCreate();
|
||||||
|
if (!decoder) {
|
||||||
|
PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder");
|
||||||
|
PyBuffer_Release(&buffer);
|
||||||
|
PyObject_Del(self);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
decoder->maxThreads = max_threads;
|
||||||
|
// Turn off libavif's 'clap' (clean aperture) property validation.
|
||||||
|
decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID;
|
||||||
|
// Allow the PixelInformationProperty ('pixi') to be missing in AV1 image
|
||||||
|
// items. libheif v1.11.0 and older does not add the 'pixi' item property to
|
||||||
|
// AV1 image items.
|
||||||
|
decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED;
|
||||||
|
decoder->codecChoice = codec;
|
||||||
|
|
||||||
|
result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Setting IO memory failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
avifDecoderDestroy(decoder);
|
||||||
|
PyBuffer_Release(&buffer);
|
||||||
|
PyObject_Del(self);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = avifDecoderParse(decoder);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Failed to decode image: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
avifDecoderDestroy(decoder);
|
||||||
|
PyBuffer_Release(&buffer);
|
||||||
|
PyObject_Del(self);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->decoder = decoder;
|
||||||
|
self->buffer = buffer;
|
||||||
|
|
||||||
|
return (PyObject *)self;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_decoder_dealloc(AvifDecoderObject *self) {
|
||||||
|
if (self->decoder) {
|
||||||
|
avifDecoderDestroy(self->decoder);
|
||||||
|
}
|
||||||
|
PyBuffer_Release(&self->buffer);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_decoder_get_info(AvifDecoderObject *self) {
|
||||||
|
avifDecoder *decoder = self->decoder;
|
||||||
|
avifImage *image = decoder->image;
|
||||||
|
|
||||||
|
PyObject *icc = NULL;
|
||||||
|
PyObject *exif = NULL;
|
||||||
|
PyObject *xmp = NULL;
|
||||||
|
PyObject *ret = NULL;
|
||||||
|
|
||||||
|
if (image->xmp.size) {
|
||||||
|
xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image->exif.size) {
|
||||||
|
exif =
|
||||||
|
PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image->icc.size) {
|
||||||
|
icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = Py_BuildValue(
|
||||||
|
"(II)IsSSIS",
|
||||||
|
image->width,
|
||||||
|
image->height,
|
||||||
|
decoder->imageCount,
|
||||||
|
decoder->alphaPresent ? "RGBA" : "RGB",
|
||||||
|
NULL == icc ? Py_None : icc,
|
||||||
|
NULL == exif ? Py_None : exif,
|
||||||
|
irot_imir_to_exif_orientation(image),
|
||||||
|
NULL == xmp ? Py_None : xmp
|
||||||
|
);
|
||||||
|
|
||||||
|
Py_XDECREF(xmp);
|
||||||
|
Py_XDECREF(exif);
|
||||||
|
Py_XDECREF(icc);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
_decoder_get_frame(AvifDecoderObject *self, PyObject *args) {
|
||||||
|
PyObject *bytes;
|
||||||
|
PyObject *ret;
|
||||||
|
Py_ssize_t size;
|
||||||
|
avifResult result;
|
||||||
|
avifRGBImage rgb;
|
||||||
|
avifDecoder *decoder;
|
||||||
|
avifImage *image;
|
||||||
|
uint32_t frame_index;
|
||||||
|
|
||||||
|
decoder = self->decoder;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(args, "I", &frame_index)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = avifDecoderNthImage(decoder, frame_index);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Failed to decode frame %u: %s",
|
||||||
|
frame_index,
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
image = decoder->image;
|
||||||
|
|
||||||
|
avifRGBImageSetDefaults(&rgb, image);
|
||||||
|
|
||||||
|
rgb.depth = 8;
|
||||||
|
rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB;
|
||||||
|
|
||||||
|
result = avifRGBImageAllocatePixels(&rgb);
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Pixel allocation failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_BEGIN_ALLOW_THREADS;
|
||||||
|
result = avifImageYUVToRGB(image, &rgb);
|
||||||
|
Py_END_ALLOW_THREADS;
|
||||||
|
|
||||||
|
if (result != AVIF_RESULT_OK) {
|
||||||
|
PyErr_Format(
|
||||||
|
exc_type_for_avif_result(result),
|
||||||
|
"Conversion from YUV failed: %s",
|
||||||
|
avifResultToString(result)
|
||||||
|
);
|
||||||
|
avifRGBImageFreePixels(&rgb);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) {
|
||||||
|
PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = rgb.rowBytes * rgb.height;
|
||||||
|
|
||||||
|
bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size);
|
||||||
|
avifRGBImageFreePixels(&rgb);
|
||||||
|
|
||||||
|
ret = Py_BuildValue(
|
||||||
|
"SKKK",
|
||||||
|
bytes,
|
||||||
|
decoder->timescale,
|
||||||
|
decoder->imageTiming.ptsInTimescales,
|
||||||
|
decoder->imageTiming.durationInTimescales
|
||||||
|
);
|
||||||
|
|
||||||
|
Py_DECREF(bytes);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Type Definitions */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
// AvifEncoder methods
|
||||||
|
static struct PyMethodDef _encoder_methods[] = {
|
||||||
|
{"add", (PyCFunction)_encoder_add, METH_VARARGS},
|
||||||
|
{"finish", (PyCFunction)_encoder_finish, METH_NOARGS},
|
||||||
|
{NULL, NULL} /* sentinel */
|
||||||
|
};
|
||||||
|
|
||||||
|
// AvifEncoder type definition
|
||||||
|
static PyTypeObject AvifEncoder_Type = {
|
||||||
|
PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder",
|
||||||
|
.tp_basicsize = sizeof(AvifEncoderObject),
|
||||||
|
.tp_dealloc = (destructor)_encoder_dealloc,
|
||||||
|
.tp_methods = _encoder_methods,
|
||||||
|
};
|
||||||
|
|
||||||
|
// AvifDecoder methods
|
||||||
|
static struct PyMethodDef _decoder_methods[] = {
|
||||||
|
{"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS},
|
||||||
|
{"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS},
|
||||||
|
{NULL, NULL} /* sentinel */
|
||||||
|
};
|
||||||
|
|
||||||
|
// AvifDecoder type definition
|
||||||
|
static PyTypeObject AvifDecoder_Type = {
|
||||||
|
PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder",
|
||||||
|
.tp_basicsize = sizeof(AvifDecoderObject),
|
||||||
|
.tp_dealloc = (destructor)_decoder_dealloc,
|
||||||
|
.tp_methods = _decoder_methods,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Module Setup */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static PyMethodDef avifMethods[] = {
|
||||||
|
{"AvifDecoder", AvifDecoderNew, METH_VARARGS},
|
||||||
|
{"AvifEncoder", AvifEncoderNew, METH_VARARGS},
|
||||||
|
{"decoder_codec_available", _decoder_codec_available, METH_VARARGS},
|
||||||
|
{"encoder_codec_available", _encoder_codec_available, METH_VARARGS},
|
||||||
|
{"codec_versions", _codec_versions, METH_NOARGS},
|
||||||
|
{NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
static int
|
||||||
|
setup_module(PyObject *m) {
|
||||||
|
if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *d = PyModule_GetDict(m);
|
||||||
|
PyObject *v = PyUnicode_FromString(avifVersion());
|
||||||
|
PyDict_SetItemString(d, "libavif_version", v ? v : Py_None);
|
||||||
|
Py_XDECREF(v);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyMODINIT_FUNC
|
||||||
|
PyInit__avif(void) {
|
||||||
|
PyObject *m;
|
||||||
|
|
||||||
|
static PyModuleDef module_def = {
|
||||||
|
PyModuleDef_HEAD_INIT,
|
||||||
|
.m_name = "_avif",
|
||||||
|
.m_size = -1,
|
||||||
|
.m_methods = avifMethods,
|
||||||
|
};
|
||||||
|
|
||||||
|
m = PyModule_Create(&module_def);
|
||||||
|
if (setup_module(m) < 0) {
|
||||||
|
Py_DECREF(m);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Py_GIL_DISABLED
|
||||||
|
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return m;
|
||||||
|
}
|
115
src/_imaging.c
115
src/_imaging.c
|
@ -230,6 +230,93 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) {
|
||||||
return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE);
|
return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
/* Arrow HANDLING */
|
||||||
|
/* -------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
ArrowError(int err) {
|
||||||
|
if (err == IMAGING_CODEC_MEMORY) {
|
||||||
|
return ImagingError_MemoryError();
|
||||||
|
}
|
||||||
|
if (err == IMAGING_ARROW_INCOMPATIBLE_MODE) {
|
||||||
|
return ImagingError_ValueError("Incompatible Pillow mode for Arrow array");
|
||||||
|
}
|
||||||
|
if (err == IMAGING_ARROW_MEMORY_LAYOUT) {
|
||||||
|
return ImagingError_ValueError(
|
||||||
|
"Image is in multiple array blocks, use imaging_new_block for zero copy"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ImagingError_ValueError("Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ReleaseArrowSchemaPyCapsule(PyObject *capsule) {
|
||||||
|
struct ArrowSchema *schema =
|
||||||
|
(struct ArrowSchema *)PyCapsule_GetPointer(capsule, "arrow_schema");
|
||||||
|
if (schema->release != NULL) {
|
||||||
|
schema->release(schema);
|
||||||
|
}
|
||||||
|
free(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
ExportArrowSchemaPyCapsule(ImagingObject *self) {
|
||||||
|
struct ArrowSchema *schema =
|
||||||
|
(struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema));
|
||||||
|
int err = export_imaging_schema(self->image, schema);
|
||||||
|
if (err == 0) {
|
||||||
|
return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule);
|
||||||
|
}
|
||||||
|
free(schema);
|
||||||
|
return ArrowError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ReleaseArrowArrayPyCapsule(PyObject *capsule) {
|
||||||
|
struct ArrowArray *array =
|
||||||
|
(struct ArrowArray *)PyCapsule_GetPointer(capsule, "arrow_array");
|
||||||
|
if (array->release != NULL) {
|
||||||
|
array->release(array);
|
||||||
|
}
|
||||||
|
free(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject *
|
||||||
|
ExportArrowArrayPyCapsule(ImagingObject *self) {
|
||||||
|
struct ArrowArray *array =
|
||||||
|
(struct ArrowArray *)calloc(1, sizeof(struct ArrowArray));
|
||||||
|
int err = export_imaging_array(self->image, array);
|
||||||
|
if (err == 0) {
|
||||||
|
return PyCapsule_New(array, "arrow_array", ReleaseArrowArrayPyCapsule);
|
||||||
|
}
|
||||||
|
free(array);
|
||||||
|
return ArrowError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
_new_arrow(PyObject *self, PyObject *args) {
|
||||||
|
char *mode;
|
||||||
|
int xsize, ysize;
|
||||||
|
PyObject *schema_capsule, *array_capsule;
|
||||||
|
PyObject *ret;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTuple(
|
||||||
|
args, "s(ii)OO", &mode, &xsize, &ysize, &schema_capsule, &array_capsule
|
||||||
|
)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImagingBorrowArrow is responsible for retaining the array_capsule
|
||||||
|
ret =
|
||||||
|
PyImagingNew(ImagingNewArrow(mode, xsize, ysize, schema_capsule, array_capsule)
|
||||||
|
);
|
||||||
|
if (!ret) {
|
||||||
|
return ImagingError_ValueError("Invalid Arrow array mode or size mismatch");
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
/* EXCEPTION REROUTING */
|
/* EXCEPTION REROUTING */
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
|
@ -3655,6 +3742,10 @@ static struct PyMethodDef methods[] = {
|
||||||
/* Misc. */
|
/* Misc. */
|
||||||
{"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS},
|
{"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS},
|
||||||
|
|
||||||
|
/* arrow */
|
||||||
|
{"__arrow_c_schema__", (PyCFunction)ExportArrowSchemaPyCapsule, METH_VARARGS},
|
||||||
|
{"__arrow_c_array__", (PyCFunction)ExportArrowArrayPyCapsule, METH_VARARGS},
|
||||||
|
|
||||||
{NULL, NULL} /* sentinel */
|
{NULL, NULL} /* sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3722,6 +3813,11 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
_getattr_readonly(ImagingObject *self, void *closure) {
|
||||||
|
return PyLong_FromLong(self->image->read_only);
|
||||||
|
}
|
||||||
|
|
||||||
static struct PyGetSetDef getsetters[] = {
|
static struct PyGetSetDef getsetters[] = {
|
||||||
{"mode", (getter)_getattr_mode},
|
{"mode", (getter)_getattr_mode},
|
||||||
{"size", (getter)_getattr_size},
|
{"size", (getter)_getattr_size},
|
||||||
|
@ -3729,6 +3825,7 @@ static struct PyGetSetDef getsetters[] = {
|
||||||
{"id", (getter)_getattr_id},
|
{"id", (getter)_getattr_id},
|
||||||
{"ptr", (getter)_getattr_ptr},
|
{"ptr", (getter)_getattr_ptr},
|
||||||
{"unsafe_ptrs", (getter)_getattr_unsafe_ptrs},
|
{"unsafe_ptrs", (getter)_getattr_unsafe_ptrs},
|
||||||
|
{"readonly", (getter)_getattr_readonly},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3983,6 +4080,21 @@ _set_blocks_max(PyObject *self, PyObject *args) {
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
_set_use_block_allocator(PyObject *self, PyObject *args) {
|
||||||
|
int use_block_allocator;
|
||||||
|
if (!PyArg_ParseTuple(args, "i:set_use_block_allocator", &use_block_allocator)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
ImagingMemorySetBlockAllocator(&ImagingDefaultArena, use_block_allocator);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
_get_use_block_allocator(PyObject *self, PyObject *args) {
|
||||||
|
return PyLong_FromLong(ImagingDefaultArena.use_block_allocator);
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_clear_cache(PyObject *self, PyObject *args) {
|
_clear_cache(PyObject *self, PyObject *args) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
@ -4104,6 +4216,7 @@ static PyMethodDef functions[] = {
|
||||||
{"fill", (PyCFunction)_fill, METH_VARARGS},
|
{"fill", (PyCFunction)_fill, METH_VARARGS},
|
||||||
{"new", (PyCFunction)_new, METH_VARARGS},
|
{"new", (PyCFunction)_new, METH_VARARGS},
|
||||||
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
|
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
|
||||||
|
{"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS},
|
||||||
{"merge", (PyCFunction)_merge, METH_VARARGS},
|
{"merge", (PyCFunction)_merge, METH_VARARGS},
|
||||||
|
|
||||||
/* Functions */
|
/* Functions */
|
||||||
|
@ -4190,9 +4303,11 @@ static PyMethodDef functions[] = {
|
||||||
{"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS},
|
{"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS},
|
||||||
{"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS},
|
{"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS},
|
||||||
{"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS},
|
{"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS},
|
||||||
|
{"get_use_block_allocator", (PyCFunction)_get_use_block_allocator, METH_VARARGS},
|
||||||
{"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS},
|
{"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS},
|
||||||
{"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS},
|
{"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS},
|
||||||
{"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS},
|
{"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS},
|
||||||
|
{"set_use_block_allocator", (PyCFunction)_set_use_block_allocator, METH_VARARGS},
|
||||||
{"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS},
|
{"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS},
|
||||||
|
|
||||||
{NULL, NULL} /* sentinel */
|
{NULL, NULL} /* sentinel */
|
||||||
|
|
299
src/libImaging/Arrow.c
Normal file
299
src/libImaging/Arrow.c
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
|
||||||
|
#include "Arrow.h"
|
||||||
|
#include "Imaging.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* struct ArrowSchema* */
|
||||||
|
/* _arrow_schema_channel(char* channel, char* format) { */
|
||||||
|
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
static void
|
||||||
|
ReleaseExportedSchema(struct ArrowSchema *array) {
|
||||||
|
// This should not be called on already released array
|
||||||
|
// assert(array->release != NULL);
|
||||||
|
|
||||||
|
if (!array->release) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (array->format) {
|
||||||
|
free((void *)array->format);
|
||||||
|
array->format = NULL;
|
||||||
|
}
|
||||||
|
if (array->name) {
|
||||||
|
free((void *)array->name);
|
||||||
|
array->name = NULL;
|
||||||
|
}
|
||||||
|
if (array->metadata) {
|
||||||
|
free((void *)array->metadata);
|
||||||
|
array->metadata = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release children
|
||||||
|
for (int64_t i = 0; i < array->n_children; ++i) {
|
||||||
|
struct ArrowSchema *child = array->children[i];
|
||||||
|
if (child->release != NULL) {
|
||||||
|
child->release(child);
|
||||||
|
child->release = NULL;
|
||||||
|
}
|
||||||
|
// UNDONE -- should I be releasing the children?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release dictionary
|
||||||
|
struct ArrowSchema *dict = array->dictionary;
|
||||||
|
if (dict != NULL && dict->release != NULL) {
|
||||||
|
dict->release(dict);
|
||||||
|
dict->release = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO here: release and/or deallocate all data directly owned by
|
||||||
|
// the ArrowArray struct, such as the private_data.
|
||||||
|
|
||||||
|
// Mark array released
|
||||||
|
array->release = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
export_named_type(struct ArrowSchema *schema, char *format, char *name) {
|
||||||
|
char *formatp;
|
||||||
|
char *namep;
|
||||||
|
size_t format_len = strlen(format) + 1;
|
||||||
|
size_t name_len = strlen(name) + 1;
|
||||||
|
|
||||||
|
formatp = calloc(format_len, 1);
|
||||||
|
|
||||||
|
if (!formatp) {
|
||||||
|
return IMAGING_CODEC_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
namep = calloc(name_len, 1);
|
||||||
|
if (!namep) {
|
||||||
|
free(formatp);
|
||||||
|
return IMAGING_CODEC_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(formatp, format, format_len);
|
||||||
|
strncpy(namep, name, name_len);
|
||||||
|
|
||||||
|
*schema = (struct ArrowSchema){// Type description
|
||||||
|
.format = formatp,
|
||||||
|
.name = namep,
|
||||||
|
.metadata = NULL,
|
||||||
|
.flags = 0,
|
||||||
|
.n_children = 0,
|
||||||
|
.children = NULL,
|
||||||
|
.dictionary = NULL,
|
||||||
|
// Bookkeeping
|
||||||
|
.release = &ReleaseExportedSchema
|
||||||
|
};
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
export_imaging_schema(Imaging im, struct ArrowSchema *schema) {
|
||||||
|
int retval = 0;
|
||||||
|
|
||||||
|
if (strcmp(im->arrow_band_format, "") == 0) {
|
||||||
|
return IMAGING_ARROW_INCOMPATIBLE_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for now, single block images */
|
||||||
|
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
|
||||||
|
return IMAGING_ARROW_MEMORY_LAYOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (im->bands == 1) {
|
||||||
|
return export_named_type(schema, im->arrow_band_format, im->band_names[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
retval = export_named_type(schema, "+w:4", "");
|
||||||
|
if (retval != 0) {
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
// if it's not 1 band, it's an int32 at the moment. 4 uint8 bands.
|
||||||
|
schema->n_children = 1;
|
||||||
|
schema->children = calloc(1, sizeof(struct ArrowSchema *));
|
||||||
|
schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema));
|
||||||
|
retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel");
|
||||||
|
if (retval != 0) {
|
||||||
|
free(schema->children[0]);
|
||||||
|
schema->release(schema);
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
release_const_array(struct ArrowArray *array) {
|
||||||
|
Imaging im = (Imaging)array->private_data;
|
||||||
|
|
||||||
|
if (array->n_children == 0) {
|
||||||
|
ImagingDelete(im);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the buffers and the buffers array
|
||||||
|
if (array->buffers) {
|
||||||
|
free(array->buffers);
|
||||||
|
array->buffers = NULL;
|
||||||
|
}
|
||||||
|
if (array->children) {
|
||||||
|
// undone -- does arrow release all the children recursively?
|
||||||
|
for (int i = 0; i < array->n_children; i++) {
|
||||||
|
if (array->children[i]->release) {
|
||||||
|
array->children[i]->release(array->children[i]);
|
||||||
|
array->children[i]->release = NULL;
|
||||||
|
free(array->children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(array->children);
|
||||||
|
array->children = NULL;
|
||||||
|
}
|
||||||
|
// Mark released
|
||||||
|
array->release = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
export_single_channel_array(Imaging im, struct ArrowArray *array) {
|
||||||
|
int length = im->xsize * im->ysize;
|
||||||
|
|
||||||
|
/* for now, single block images */
|
||||||
|
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
|
||||||
|
return IMAGING_ARROW_MEMORY_LAYOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (im->lines_per_block && im->lines_per_block < im->ysize) {
|
||||||
|
length = im->xsize * im->lines_per_block;
|
||||||
|
}
|
||||||
|
|
||||||
|
MUTEX_LOCK(&im->mutex);
|
||||||
|
im->refcount++;
|
||||||
|
MUTEX_UNLOCK(&im->mutex);
|
||||||
|
// Initialize primitive fields
|
||||||
|
*array = (struct ArrowArray){// Data description
|
||||||
|
.length = length,
|
||||||
|
.offset = 0,
|
||||||
|
.null_count = 0,
|
||||||
|
.n_buffers = 2,
|
||||||
|
.n_children = 0,
|
||||||
|
.children = NULL,
|
||||||
|
.dictionary = NULL,
|
||||||
|
// Bookkeeping
|
||||||
|
.release = &release_const_array,
|
||||||
|
.private_data = im
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allocate list of buffers
|
||||||
|
array->buffers = (const void **)malloc(sizeof(void *) * array->n_buffers);
|
||||||
|
// assert(array->buffers != NULL);
|
||||||
|
array->buffers[0] = NULL; // no nulls, null bitmap can be omitted
|
||||||
|
|
||||||
|
if (im->block) {
|
||||||
|
array->buffers[1] = im->block;
|
||||||
|
} else {
|
||||||
|
array->buffers[1] = im->blocks[0].ptr;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
export_fixed_pixel_array(Imaging im, struct ArrowArray *array) {
|
||||||
|
int length = im->xsize * im->ysize;
|
||||||
|
|
||||||
|
/* for now, single block images */
|
||||||
|
if (!(im->blocks_count == 0 || im->blocks_count == 1)) {
|
||||||
|
return IMAGING_ARROW_MEMORY_LAYOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (im->lines_per_block && im->lines_per_block < im->ysize) {
|
||||||
|
length = im->xsize * im->lines_per_block;
|
||||||
|
}
|
||||||
|
|
||||||
|
MUTEX_LOCK(&im->mutex);
|
||||||
|
im->refcount++;
|
||||||
|
MUTEX_UNLOCK(&im->mutex);
|
||||||
|
// Initialize primitive fields
|
||||||
|
// Fixed length arrays are 1 buffer of validity, and the length in pixels.
|
||||||
|
// Data is in a child array.
|
||||||
|
*array = (struct ArrowArray){// Data description
|
||||||
|
.length = length,
|
||||||
|
.offset = 0,
|
||||||
|
.null_count = 0,
|
||||||
|
.n_buffers = 1,
|
||||||
|
.n_children = 1,
|
||||||
|
.children = NULL,
|
||||||
|
.dictionary = NULL,
|
||||||
|
// Bookkeeping
|
||||||
|
.release = &release_const_array,
|
||||||
|
.private_data = im
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allocate list of buffers
|
||||||
|
array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers);
|
||||||
|
if (!array->buffers) {
|
||||||
|
goto err;
|
||||||
|
}
|
||||||
|
// assert(array->buffers != NULL);
|
||||||
|
array->buffers[0] = NULL; // no nulls, null bitmap can be omitted
|
||||||
|
|
||||||
|
// if it's not 1 band, it's an int32 at the moment. 4 uint8 bands.
|
||||||
|
array->n_children = 1;
|
||||||
|
array->children = calloc(1, sizeof(struct ArrowArray *));
|
||||||
|
if (!array->children) {
|
||||||
|
goto err;
|
||||||
|
}
|
||||||
|
array->children[0] = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray));
|
||||||
|
if (!array->children[0]) {
|
||||||
|
goto err;
|
||||||
|
}
|
||||||
|
|
||||||
|
MUTEX_LOCK(&im->mutex);
|
||||||
|
im->refcount++;
|
||||||
|
MUTEX_UNLOCK(&im->mutex);
|
||||||
|
*array->children[0] = (struct ArrowArray){// Data description
|
||||||
|
.length = length * 4,
|
||||||
|
.offset = 0,
|
||||||
|
.null_count = 0,
|
||||||
|
.n_buffers = 2,
|
||||||
|
.n_children = 0,
|
||||||
|
.children = NULL,
|
||||||
|
.dictionary = NULL,
|
||||||
|
// Bookkeeping
|
||||||
|
.release = &release_const_array,
|
||||||
|
.private_data = im
|
||||||
|
};
|
||||||
|
|
||||||
|
array->children[0]->buffers =
|
||||||
|
(const void **)calloc(2, sizeof(void *) * array->n_buffers);
|
||||||
|
|
||||||
|
if (im->block) {
|
||||||
|
array->children[0]->buffers[1] = im->block;
|
||||||
|
} else {
|
||||||
|
array->children[0]->buffers[1] = im->blocks[0].ptr;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
err:
|
||||||
|
if (array->children[0]) {
|
||||||
|
free(array->children[0]);
|
||||||
|
}
|
||||||
|
if (array->children) {
|
||||||
|
free(array->children);
|
||||||
|
}
|
||||||
|
if (array->buffers) {
|
||||||
|
free(array->buffers);
|
||||||
|
}
|
||||||
|
return IMAGING_CODEC_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
export_imaging_array(Imaging im, struct ArrowArray *array) {
|
||||||
|
if (strcmp(im->arrow_band_format, "") == 0) {
|
||||||
|
return IMAGING_ARROW_INCOMPATIBLE_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (im->bands == 1) {
|
||||||
|
return export_single_channel_array(im, array);
|
||||||
|
}
|
||||||
|
|
||||||
|
return export_fixed_pixel_array(im, array);
|
||||||
|
}
|
48
src/libImaging/Arrow.h
Normal file
48
src/libImaging/Arrow.h
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
// Apache License 2.0.
|
||||||
|
// Source apache arrow project
|
||||||
|
// https://arrow.apache.org/docs/format/CDataInterface.html
|
||||||
|
|
||||||
|
#ifndef ARROW_C_DATA_INTERFACE
|
||||||
|
#define ARROW_C_DATA_INTERFACE
|
||||||
|
|
||||||
|
#define ARROW_FLAG_DICTIONARY_ORDERED 1
|
||||||
|
#define ARROW_FLAG_NULLABLE 2
|
||||||
|
#define ARROW_FLAG_MAP_KEYS_SORTED 4
|
||||||
|
|
||||||
|
struct ArrowSchema {
|
||||||
|
// Array type description
|
||||||
|
const char *format;
|
||||||
|
const char *name;
|
||||||
|
const char *metadata;
|
||||||
|
int64_t flags;
|
||||||
|
int64_t n_children;
|
||||||
|
struct ArrowSchema **children;
|
||||||
|
struct ArrowSchema *dictionary;
|
||||||
|
|
||||||
|
// Release callback
|
||||||
|
void (*release)(struct ArrowSchema *);
|
||||||
|
// Opaque producer-specific data
|
||||||
|
void *private_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArrowArray {
|
||||||
|
// Array data description
|
||||||
|
int64_t length;
|
||||||
|
int64_t null_count;
|
||||||
|
int64_t offset;
|
||||||
|
int64_t n_buffers;
|
||||||
|
int64_t n_children;
|
||||||
|
const void **buffers;
|
||||||
|
struct ArrowArray **children;
|
||||||
|
struct ArrowArray *dictionary;
|
||||||
|
|
||||||
|
// Release callback
|
||||||
|
void (*release)(struct ArrowArray *);
|
||||||
|
// Opaque producer-specific data
|
||||||
|
void *private_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ARROW_C_DATA_INTERFACE
|
|
@ -20,6 +20,8 @@ extern "C" {
|
||||||
#define M_PI 3.1415926535897932384626433832795
|
#define M_PI 3.1415926535897932384626433832795
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "Arrow.h"
|
||||||
|
|
||||||
/* -------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------- */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -104,6 +106,21 @@ struct ImagingMemoryInstance {
|
||||||
|
|
||||||
/* Virtual methods */
|
/* Virtual methods */
|
||||||
void (*destroy)(Imaging im);
|
void (*destroy)(Imaging im);
|
||||||
|
|
||||||
|
/* arrow */
|
||||||
|
int refcount; /* Number of arrow arrays that have been allocated */
|
||||||
|
char band_names[4][3]; /* names of bands, max 2 char + null terminator */
|
||||||
|
char arrow_band_format[2]; /* single character + null terminator */
|
||||||
|
|
||||||
|
int read_only; /* flag for read-only. set for arrow borrowed arrays */
|
||||||
|
PyObject *arrow_array_capsule; /* upstream arrow array source */
|
||||||
|
|
||||||
|
int blocks_count; /* Number of blocks that have been allocated */
|
||||||
|
int lines_per_block; /* Number of lines in a block have been allocated */
|
||||||
|
|
||||||
|
#ifdef Py_GIL_DISABLED
|
||||||
|
PyMutex mutex;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
#define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)])
|
#define IMAGING_PIXEL_1(im, x, y) ((im)->image8[(y)][(x)])
|
||||||
|
@ -161,6 +178,7 @@ typedef struct ImagingMemoryArena {
|
||||||
int stats_reallocated_blocks; /* Number of blocks which were actually reallocated
|
int stats_reallocated_blocks; /* Number of blocks which were actually reallocated
|
||||||
after retrieving */
|
after retrieving */
|
||||||
int stats_freed_blocks; /* Number of freed blocks */
|
int stats_freed_blocks; /* Number of freed blocks */
|
||||||
|
int use_block_allocator; /* don't use arena, use block allocator */
|
||||||
#ifdef Py_GIL_DISABLED
|
#ifdef Py_GIL_DISABLED
|
||||||
PyMutex mutex;
|
PyMutex mutex;
|
||||||
#endif
|
#endif
|
||||||
|
@ -174,6 +192,8 @@ extern int
|
||||||
ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max);
|
ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max);
|
||||||
extern void
|
extern void
|
||||||
ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size);
|
ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size);
|
||||||
|
extern void
|
||||||
|
ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator);
|
||||||
|
|
||||||
extern Imaging
|
extern Imaging
|
||||||
ImagingNew(const char *mode, int xsize, int ysize);
|
ImagingNew(const char *mode, int xsize, int ysize);
|
||||||
|
@ -187,6 +207,15 @@ ImagingDelete(Imaging im);
|
||||||
extern Imaging
|
extern Imaging
|
||||||
ImagingNewBlock(const char *mode, int xsize, int ysize);
|
ImagingNewBlock(const char *mode, int xsize, int ysize);
|
||||||
|
|
||||||
|
extern Imaging
|
||||||
|
ImagingNewArrow(
|
||||||
|
const char *mode,
|
||||||
|
int xsize,
|
||||||
|
int ysize,
|
||||||
|
PyObject *schema_capsule,
|
||||||
|
PyObject *array_capsule
|
||||||
|
);
|
||||||
|
|
||||||
extern Imaging
|
extern Imaging
|
||||||
ImagingNewPrologue(const char *mode, int xsize, int ysize);
|
ImagingNewPrologue(const char *mode, int xsize, int ysize);
|
||||||
extern Imaging
|
extern Imaging
|
||||||
|
@ -700,6 +729,13 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence);
|
||||||
extern Py_ssize_t
|
extern Py_ssize_t
|
||||||
_imaging_tell_pyFd(PyObject *fd);
|
_imaging_tell_pyFd(PyObject *fd);
|
||||||
|
|
||||||
|
/* Arrow */
|
||||||
|
|
||||||
|
extern int
|
||||||
|
export_imaging_array(Imaging im, struct ArrowArray *array);
|
||||||
|
extern int
|
||||||
|
export_imaging_schema(Imaging im, struct ArrowSchema *schema);
|
||||||
|
|
||||||
/* Errcodes */
|
/* Errcodes */
|
||||||
#define IMAGING_CODEC_END 1
|
#define IMAGING_CODEC_END 1
|
||||||
#define IMAGING_CODEC_OVERRUN -1
|
#define IMAGING_CODEC_OVERRUN -1
|
||||||
|
@ -707,6 +743,8 @@ _imaging_tell_pyFd(PyObject *fd);
|
||||||
#define IMAGING_CODEC_UNKNOWN -3
|
#define IMAGING_CODEC_UNKNOWN -3
|
||||||
#define IMAGING_CODEC_CONFIG -8
|
#define IMAGING_CODEC_CONFIG -8
|
||||||
#define IMAGING_CODEC_MEMORY -9
|
#define IMAGING_CODEC_MEMORY -9
|
||||||
|
#define IMAGING_ARROW_INCOMPATIBLE_MODE -10
|
||||||
|
#define IMAGING_ARROW_MEMORY_LAYOUT -11
|
||||||
|
|
||||||
#include "ImagingUtils.h"
|
#include "ImagingUtils.h"
|
||||||
extern UINT8 *clip8_lookups;
|
extern UINT8 *clip8_lookups;
|
||||||
|
|
|
@ -58,19 +58,22 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
/* Setup image descriptor */
|
/* Setup image descriptor */
|
||||||
im->xsize = xsize;
|
im->xsize = xsize;
|
||||||
im->ysize = ysize;
|
im->ysize = ysize;
|
||||||
|
im->refcount = 1;
|
||||||
im->type = IMAGING_TYPE_UINT8;
|
im->type = IMAGING_TYPE_UINT8;
|
||||||
|
strcpy(im->arrow_band_format, "C");
|
||||||
|
|
||||||
if (strcmp(mode, "1") == 0) {
|
if (strcmp(mode, "1") == 0) {
|
||||||
/* 1-bit images */
|
/* 1-bit images */
|
||||||
im->bands = im->pixelsize = 1;
|
im->bands = im->pixelsize = 1;
|
||||||
im->linesize = xsize;
|
im->linesize = xsize;
|
||||||
|
strcpy(im->band_names[0], "1");
|
||||||
|
|
||||||
} else if (strcmp(mode, "P") == 0) {
|
} else if (strcmp(mode, "P") == 0) {
|
||||||
/* 8-bit palette mapped images */
|
/* 8-bit palette mapped images */
|
||||||
im->bands = im->pixelsize = 1;
|
im->bands = im->pixelsize = 1;
|
||||||
im->linesize = xsize;
|
im->linesize = xsize;
|
||||||
im->palette = ImagingPaletteNew("RGB");
|
im->palette = ImagingPaletteNew("RGB");
|
||||||
|
strcpy(im->band_names[0], "P");
|
||||||
|
|
||||||
} else if (strcmp(mode, "PA") == 0) {
|
} else if (strcmp(mode, "PA") == 0) {
|
||||||
/* 8-bit palette with alpha */
|
/* 8-bit palette with alpha */
|
||||||
|
@ -78,23 +81,36 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->pixelsize = 4; /* store in image32 memory */
|
im->pixelsize = 4; /* store in image32 memory */
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
im->palette = ImagingPaletteNew("RGB");
|
im->palette = ImagingPaletteNew("RGB");
|
||||||
|
strcpy(im->band_names[0], "P");
|
||||||
|
strcpy(im->band_names[1], "X");
|
||||||
|
strcpy(im->band_names[2], "X");
|
||||||
|
strcpy(im->band_names[3], "A");
|
||||||
|
|
||||||
} else if (strcmp(mode, "L") == 0) {
|
} else if (strcmp(mode, "L") == 0) {
|
||||||
/* 8-bit grayscale (luminance) images */
|
/* 8-bit grayscale (luminance) images */
|
||||||
im->bands = im->pixelsize = 1;
|
im->bands = im->pixelsize = 1;
|
||||||
im->linesize = xsize;
|
im->linesize = xsize;
|
||||||
|
strcpy(im->band_names[0], "L");
|
||||||
|
|
||||||
} else if (strcmp(mode, "LA") == 0) {
|
} else if (strcmp(mode, "LA") == 0) {
|
||||||
/* 8-bit grayscale (luminance) with alpha */
|
/* 8-bit grayscale (luminance) with alpha */
|
||||||
im->bands = 2;
|
im->bands = 2;
|
||||||
im->pixelsize = 4; /* store in image32 memory */
|
im->pixelsize = 4; /* store in image32 memory */
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "L");
|
||||||
|
strcpy(im->band_names[1], "X");
|
||||||
|
strcpy(im->band_names[2], "X");
|
||||||
|
strcpy(im->band_names[3], "A");
|
||||||
|
|
||||||
} else if (strcmp(mode, "La") == 0) {
|
} else if (strcmp(mode, "La") == 0) {
|
||||||
/* 8-bit grayscale (luminance) with premultiplied alpha */
|
/* 8-bit grayscale (luminance) with premultiplied alpha */
|
||||||
im->bands = 2;
|
im->bands = 2;
|
||||||
im->pixelsize = 4; /* store in image32 memory */
|
im->pixelsize = 4; /* store in image32 memory */
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "L");
|
||||||
|
strcpy(im->band_names[1], "X");
|
||||||
|
strcpy(im->band_names[2], "X");
|
||||||
|
strcpy(im->band_names[3], "a");
|
||||||
|
|
||||||
} else if (strcmp(mode, "F") == 0) {
|
} else if (strcmp(mode, "F") == 0) {
|
||||||
/* 32-bit floating point images */
|
/* 32-bit floating point images */
|
||||||
|
@ -102,6 +118,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->pixelsize = 4;
|
im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
im->type = IMAGING_TYPE_FLOAT32;
|
im->type = IMAGING_TYPE_FLOAT32;
|
||||||
|
strcpy(im->arrow_band_format, "f");
|
||||||
|
strcpy(im->band_names[0], "F");
|
||||||
|
|
||||||
} else if (strcmp(mode, "I") == 0) {
|
} else if (strcmp(mode, "I") == 0) {
|
||||||
/* 32-bit integer images */
|
/* 32-bit integer images */
|
||||||
|
@ -109,6 +127,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->pixelsize = 4;
|
im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
im->type = IMAGING_TYPE_INT32;
|
im->type = IMAGING_TYPE_INT32;
|
||||||
|
strcpy(im->arrow_band_format, "i");
|
||||||
|
strcpy(im->band_names[0], "I");
|
||||||
|
|
||||||
} else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 ||
|
} else if (strcmp(mode, "I;16") == 0 || strcmp(mode, "I;16L") == 0 ||
|
||||||
strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) {
|
strcmp(mode, "I;16B") == 0 || strcmp(mode, "I;16N") == 0) {
|
||||||
|
@ -118,12 +138,18 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->pixelsize = 2;
|
im->pixelsize = 2;
|
||||||
im->linesize = xsize * 2;
|
im->linesize = xsize * 2;
|
||||||
im->type = IMAGING_TYPE_SPECIAL;
|
im->type = IMAGING_TYPE_SPECIAL;
|
||||||
|
strcpy(im->arrow_band_format, "s");
|
||||||
|
strcpy(im->band_names[0], "I");
|
||||||
|
|
||||||
} else if (strcmp(mode, "RGB") == 0) {
|
} else if (strcmp(mode, "RGB") == 0) {
|
||||||
/* 24-bit true colour images */
|
/* 24-bit true colour images */
|
||||||
im->bands = 3;
|
im->bands = 3;
|
||||||
im->pixelsize = 4;
|
im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "R");
|
||||||
|
strcpy(im->band_names[1], "G");
|
||||||
|
strcpy(im->band_names[2], "B");
|
||||||
|
strcpy(im->band_names[3], "X");
|
||||||
|
|
||||||
} else if (strcmp(mode, "BGR;15") == 0) {
|
} else if (strcmp(mode, "BGR;15") == 0) {
|
||||||
/* EXPERIMENTAL */
|
/* EXPERIMENTAL */
|
||||||
|
@ -132,6 +158,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->pixelsize = 2;
|
im->pixelsize = 2;
|
||||||
im->linesize = (xsize * 2 + 3) & -4;
|
im->linesize = (xsize * 2 + 3) & -4;
|
||||||
im->type = IMAGING_TYPE_SPECIAL;
|
im->type = IMAGING_TYPE_SPECIAL;
|
||||||
|
/* not allowing arrow due to line length packing */
|
||||||
|
strcpy(im->arrow_band_format, "");
|
||||||
|
|
||||||
} else if (strcmp(mode, "BGR;16") == 0) {
|
} else if (strcmp(mode, "BGR;16") == 0) {
|
||||||
/* EXPERIMENTAL */
|
/* EXPERIMENTAL */
|
||||||
|
@ -140,6 +168,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->pixelsize = 2;
|
im->pixelsize = 2;
|
||||||
im->linesize = (xsize * 2 + 3) & -4;
|
im->linesize = (xsize * 2 + 3) & -4;
|
||||||
im->type = IMAGING_TYPE_SPECIAL;
|
im->type = IMAGING_TYPE_SPECIAL;
|
||||||
|
/* not allowing arrow due to line length packing */
|
||||||
|
strcpy(im->arrow_band_format, "");
|
||||||
|
|
||||||
} else if (strcmp(mode, "BGR;24") == 0) {
|
} else if (strcmp(mode, "BGR;24") == 0) {
|
||||||
/* EXPERIMENTAL */
|
/* EXPERIMENTAL */
|
||||||
|
@ -148,32 +178,54 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->pixelsize = 3;
|
im->pixelsize = 3;
|
||||||
im->linesize = (xsize * 3 + 3) & -4;
|
im->linesize = (xsize * 3 + 3) & -4;
|
||||||
im->type = IMAGING_TYPE_SPECIAL;
|
im->type = IMAGING_TYPE_SPECIAL;
|
||||||
|
/* not allowing arrow due to line length packing */
|
||||||
|
strcpy(im->arrow_band_format, "");
|
||||||
|
|
||||||
} else if (strcmp(mode, "RGBX") == 0) {
|
} else if (strcmp(mode, "RGBX") == 0) {
|
||||||
/* 32-bit true colour images with padding */
|
/* 32-bit true colour images with padding */
|
||||||
im->bands = im->pixelsize = 4;
|
im->bands = im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "R");
|
||||||
|
strcpy(im->band_names[1], "G");
|
||||||
|
strcpy(im->band_names[2], "B");
|
||||||
|
strcpy(im->band_names[3], "X");
|
||||||
|
|
||||||
} else if (strcmp(mode, "RGBA") == 0) {
|
} else if (strcmp(mode, "RGBA") == 0) {
|
||||||
/* 32-bit true colour images with alpha */
|
/* 32-bit true colour images with alpha */
|
||||||
im->bands = im->pixelsize = 4;
|
im->bands = im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "R");
|
||||||
|
strcpy(im->band_names[1], "G");
|
||||||
|
strcpy(im->band_names[2], "B");
|
||||||
|
strcpy(im->band_names[3], "A");
|
||||||
|
|
||||||
} else if (strcmp(mode, "RGBa") == 0) {
|
} else if (strcmp(mode, "RGBa") == 0) {
|
||||||
/* 32-bit true colour images with premultiplied alpha */
|
/* 32-bit true colour images with premultiplied alpha */
|
||||||
im->bands = im->pixelsize = 4;
|
im->bands = im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "R");
|
||||||
|
strcpy(im->band_names[1], "G");
|
||||||
|
strcpy(im->band_names[2], "B");
|
||||||
|
strcpy(im->band_names[3], "a");
|
||||||
|
|
||||||
} else if (strcmp(mode, "CMYK") == 0) {
|
} else if (strcmp(mode, "CMYK") == 0) {
|
||||||
/* 32-bit colour separation */
|
/* 32-bit colour separation */
|
||||||
im->bands = im->pixelsize = 4;
|
im->bands = im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "C");
|
||||||
|
strcpy(im->band_names[1], "M");
|
||||||
|
strcpy(im->band_names[2], "Y");
|
||||||
|
strcpy(im->band_names[3], "K");
|
||||||
|
|
||||||
} else if (strcmp(mode, "YCbCr") == 0) {
|
} else if (strcmp(mode, "YCbCr") == 0) {
|
||||||
/* 24-bit video format */
|
/* 24-bit video format */
|
||||||
im->bands = 3;
|
im->bands = 3;
|
||||||
im->pixelsize = 4;
|
im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "Y");
|
||||||
|
strcpy(im->band_names[1], "Cb");
|
||||||
|
strcpy(im->band_names[2], "Cr");
|
||||||
|
strcpy(im->band_names[3], "X");
|
||||||
|
|
||||||
} else if (strcmp(mode, "LAB") == 0) {
|
} else if (strcmp(mode, "LAB") == 0) {
|
||||||
/* 24-bit color, luminance, + 2 color channels */
|
/* 24-bit color, luminance, + 2 color channels */
|
||||||
|
@ -181,6 +233,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->bands = 3;
|
im->bands = 3;
|
||||||
im->pixelsize = 4;
|
im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "L");
|
||||||
|
strcpy(im->band_names[1], "a");
|
||||||
|
strcpy(im->band_names[2], "b");
|
||||||
|
strcpy(im->band_names[3], "X");
|
||||||
|
|
||||||
} else if (strcmp(mode, "HSV") == 0) {
|
} else if (strcmp(mode, "HSV") == 0) {
|
||||||
/* 24-bit color, luminance, + 2 color channels */
|
/* 24-bit color, luminance, + 2 color channels */
|
||||||
|
@ -188,6 +244,10 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
im->bands = 3;
|
im->bands = 3;
|
||||||
im->pixelsize = 4;
|
im->pixelsize = 4;
|
||||||
im->linesize = xsize * 4;
|
im->linesize = xsize * 4;
|
||||||
|
strcpy(im->band_names[0], "H");
|
||||||
|
strcpy(im->band_names[1], "S");
|
||||||
|
strcpy(im->band_names[2], "V");
|
||||||
|
strcpy(im->band_names[3], "X");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
free(im);
|
free(im);
|
||||||
|
@ -218,6 +278,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UNDONE -- not accurate for arrow
|
||||||
MUTEX_LOCK(&ImagingDefaultArena.mutex);
|
MUTEX_LOCK(&ImagingDefaultArena.mutex);
|
||||||
ImagingDefaultArena.stats_new_count += 1;
|
ImagingDefaultArena.stats_new_count += 1;
|
||||||
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
|
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
|
||||||
|
@ -238,8 +299,18 @@ ImagingDelete(Imaging im) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MUTEX_LOCK(&im->mutex);
|
||||||
|
im->refcount--;
|
||||||
|
|
||||||
|
if (im->refcount > 0) {
|
||||||
|
MUTEX_UNLOCK(&im->mutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MUTEX_UNLOCK(&im->mutex);
|
||||||
|
|
||||||
if (im->palette) {
|
if (im->palette) {
|
||||||
ImagingPaletteDelete(im->palette);
|
ImagingPaletteDelete(im->palette);
|
||||||
|
im->palette = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (im->destroy) {
|
if (im->destroy) {
|
||||||
|
@ -270,6 +341,7 @@ struct ImagingMemoryArena ImagingDefaultArena = {
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0, // Stats
|
0, // Stats
|
||||||
|
0, // use_block_allocator
|
||||||
#ifdef Py_GIL_DISABLED
|
#ifdef Py_GIL_DISABLED
|
||||||
{0},
|
{0},
|
||||||
#endif
|
#endif
|
||||||
|
@ -302,6 +374,11 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator) {
|
||||||
|
arena->use_block_allocator = use_block_allocator;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) {
|
ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size) {
|
||||||
while (arena->blocks_cached > new_size) {
|
while (arena->blocks_cached > new_size) {
|
||||||
|
@ -396,11 +473,13 @@ ImagingAllocateArray(Imaging im, ImagingMemoryArena arena, int dirty, int block_
|
||||||
if (lines_per_block == 0) {
|
if (lines_per_block == 0) {
|
||||||
lines_per_block = 1;
|
lines_per_block = 1;
|
||||||
}
|
}
|
||||||
|
im->lines_per_block = lines_per_block;
|
||||||
blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block;
|
blocks_count = (im->ysize + lines_per_block - 1) / lines_per_block;
|
||||||
// printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n",
|
// printf("NEW size: %dx%d, ls: %d, lpb: %d, blocks: %d\n",
|
||||||
// im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count);
|
// im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count);
|
||||||
|
|
||||||
/* One extra pointer is always NULL */
|
/* One extra pointer is always NULL */
|
||||||
|
im->blocks_count = blocks_count;
|
||||||
im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1);
|
im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1);
|
||||||
if (!im->blocks) {
|
if (!im->blocks) {
|
||||||
return (Imaging)ImagingError_MemoryError();
|
return (Imaging)ImagingError_MemoryError();
|
||||||
|
@ -487,6 +566,58 @@ ImagingAllocateBlock(Imaging im) {
|
||||||
return im;
|
return im;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Borrowed Arrow Storage Type */
|
||||||
|
/* --------------------------- */
|
||||||
|
/* Don't allocate the image. */
|
||||||
|
|
||||||
|
static void
|
||||||
|
ImagingDestroyArrow(Imaging im) {
|
||||||
|
// Rely on the internal Python destructor for the array capsule.
|
||||||
|
if (im->arrow_array_capsule) {
|
||||||
|
Py_DECREF(im->arrow_array_capsule);
|
||||||
|
im->arrow_array_capsule = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Imaging
|
||||||
|
ImagingBorrowArrow(
|
||||||
|
Imaging im,
|
||||||
|
struct ArrowArray *external_array,
|
||||||
|
int offset_width,
|
||||||
|
PyObject *arrow_capsule
|
||||||
|
) {
|
||||||
|
// offset_width is the number of char* for a single offset from arrow
|
||||||
|
Py_ssize_t y, i;
|
||||||
|
|
||||||
|
char *borrowed_buffer = NULL;
|
||||||
|
struct ArrowArray *arr = external_array;
|
||||||
|
|
||||||
|
if (arr->n_children == 1) {
|
||||||
|
arr = arr->children[0];
|
||||||
|
}
|
||||||
|
if (arr->n_buffers == 2) {
|
||||||
|
// buffer 0 is the null list
|
||||||
|
// buffer 1 is the data
|
||||||
|
borrowed_buffer = (char *)arr->buffers[1] + (offset_width * arr->offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!borrowed_buffer) {
|
||||||
|
return (Imaging
|
||||||
|
)ImagingError_ValueError("Arrow Array, exactly 2 buffers required");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (y = i = 0; y < im->ysize; y++) {
|
||||||
|
im->image[y] = borrowed_buffer + i;
|
||||||
|
i += im->linesize;
|
||||||
|
}
|
||||||
|
im->read_only = 1;
|
||||||
|
Py_INCREF(arrow_capsule);
|
||||||
|
im->arrow_array_capsule = arrow_capsule;
|
||||||
|
im->destroy = ImagingDestroyArrow;
|
||||||
|
|
||||||
|
return im;
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------------------
|
/* --------------------------------------------------------------------
|
||||||
* Create a new, internally allocated, image.
|
* Create a new, internally allocated, image.
|
||||||
*/
|
*/
|
||||||
|
@ -529,11 +660,17 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) {
|
||||||
|
|
||||||
Imaging
|
Imaging
|
||||||
ImagingNew(const char *mode, int xsize, int ysize) {
|
ImagingNew(const char *mode, int xsize, int ysize) {
|
||||||
|
if (ImagingDefaultArena.use_block_allocator) {
|
||||||
|
return ImagingNewBlock(mode, xsize, ysize);
|
||||||
|
}
|
||||||
return ImagingNewInternal(mode, xsize, ysize, 0);
|
return ImagingNewInternal(mode, xsize, ysize, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Imaging
|
Imaging
|
||||||
ImagingNewDirty(const char *mode, int xsize, int ysize) {
|
ImagingNewDirty(const char *mode, int xsize, int ysize) {
|
||||||
|
if (ImagingDefaultArena.use_block_allocator) {
|
||||||
|
return ImagingNewBlock(mode, xsize, ysize);
|
||||||
|
}
|
||||||
return ImagingNewInternal(mode, xsize, ysize, 1);
|
return ImagingNewInternal(mode, xsize, ysize, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,6 +695,66 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Imaging
|
||||||
|
ImagingNewArrow(
|
||||||
|
const char *mode,
|
||||||
|
int xsize,
|
||||||
|
int ysize,
|
||||||
|
PyObject *schema_capsule,
|
||||||
|
PyObject *array_capsule
|
||||||
|
) {
|
||||||
|
/* A borrowed arrow array */
|
||||||
|
Imaging im;
|
||||||
|
struct ArrowSchema *schema =
|
||||||
|
(struct ArrowSchema *)PyCapsule_GetPointer(schema_capsule, "arrow_schema");
|
||||||
|
|
||||||
|
struct ArrowArray *external_array =
|
||||||
|
(struct ArrowArray *)PyCapsule_GetPointer(array_capsule, "arrow_array");
|
||||||
|
|
||||||
|
if (xsize < 0 || ysize < 0) {
|
||||||
|
return (Imaging)ImagingError_ValueError("bad image size");
|
||||||
|
}
|
||||||
|
|
||||||
|
im = ImagingNewPrologue(mode, xsize, ysize);
|
||||||
|
if (!im) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t pixels = (int64_t)xsize * (int64_t)ysize;
|
||||||
|
|
||||||
|
// fmt:off // don't reformat this
|
||||||
|
if (((strcmp(schema->format, "I") == 0 // int32
|
||||||
|
&& im->pixelsize == 4 // 4xchar* storage
|
||||||
|
&& im->bands >= 2) // INT32 into any INT32 Storage mode
|
||||||
|
|| // (()||()) &&
|
||||||
|
(strcmp(schema->format, im->arrow_band_format) == 0 // same mode
|
||||||
|
&& im->bands == 1)) // Single band match
|
||||||
|
&& pixels == external_array->length) {
|
||||||
|
// one arrow element per, and it matches a pixelsize*char
|
||||||
|
if (ImagingBorrowArrow(im, external_array, im->pixelsize, array_capsule)) {
|
||||||
|
return im;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (strcmp(schema->format, "+w:4") == 0 // 4 up array
|
||||||
|
&& im->pixelsize == 4 // storage as 32 bpc
|
||||||
|
&& schema->n_children > 0 // make sure schema is well formed.
|
||||||
|
&& schema->children // make sure schema is well formed
|
||||||
|
&& strcmp(schema->children[0]->format, "C") == 0 // Expected format
|
||||||
|
&& strcmp(im->arrow_band_format, "C") == 0 // Expected Format
|
||||||
|
&& pixels == external_array->length // expected length
|
||||||
|
&& external_array->n_children == 1 // array is well formed
|
||||||
|
&& external_array->children // array is well formed
|
||||||
|
&& 4 * pixels == external_array->children[0]->length) {
|
||||||
|
// 4 up element of char into pixelsize == 4
|
||||||
|
if (ImagingBorrowArrow(im, external_array, 1, array_capsule)) {
|
||||||
|
return im;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fmt: on
|
||||||
|
ImagingDelete(im);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
Imaging
|
Imaging
|
||||||
ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) {
|
ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) {
|
||||||
/* allocate or validate output image */
|
/* allocate or validate output image */
|
||||||
|
|
26
wheels/dependency_licenses/AOM.txt
Normal file
26
wheels/dependency_licenses/AOM.txt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Copyright (c) 2016, Alliance for Open Media. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||||
|
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
23
wheels/dependency_licenses/DAV1D.txt
Normal file
23
wheels/dependency_licenses/DAV1D.txt
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
Copyright © 2018-2019, VideoLAN and dav1d authors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
387
wheels/dependency_licenses/LIBAVIF.txt
Normal file
387
wheels/dependency_licenses/LIBAVIF.txt
Normal file
|
@ -0,0 +1,387 @@
|
||||||
|
Copyright 2019 Joe Drago. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Files: src/obu.c
|
||||||
|
|
||||||
|
Copyright © 2018-2019, VideoLAN and dav1d authors
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Files: third_party/iccjpeg/*
|
||||||
|
|
||||||
|
In plain English:
|
||||||
|
|
||||||
|
1. We don't promise that this software works. (But if you find any bugs,
|
||||||
|
please let us know!)
|
||||||
|
2. You can use this software for whatever you want. You don't have to pay us.
|
||||||
|
3. You may not pretend that you wrote this software. If you use it in a
|
||||||
|
program, you must acknowledge somewhere in your documentation that
|
||||||
|
you've used the IJG code.
|
||||||
|
|
||||||
|
In legalese:
|
||||||
|
|
||||||
|
The authors make NO WARRANTY or representation, either express or implied,
|
||||||
|
with respect to this software, its quality, accuracy, merchantability, or
|
||||||
|
fitness for a particular purpose. This software is provided "AS IS", and you,
|
||||||
|
its user, assume the entire risk as to its quality and accuracy.
|
||||||
|
|
||||||
|
This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding.
|
||||||
|
All Rights Reserved except as specified below.
|
||||||
|
|
||||||
|
Permission is hereby granted to use, copy, modify, and distribute this
|
||||||
|
software (or portions thereof) for any purpose, without fee, subject to these
|
||||||
|
conditions:
|
||||||
|
(1) If any part of the source code for this software is distributed, then this
|
||||||
|
README file must be included, with this copyright and no-warranty notice
|
||||||
|
unaltered; and any additions, deletions, or changes to the original files
|
||||||
|
must be clearly indicated in accompanying documentation.
|
||||||
|
(2) If only executable code is distributed, then the accompanying
|
||||||
|
documentation must state that "this software is based in part on the work of
|
||||||
|
the Independent JPEG Group".
|
||||||
|
(3) Permission for use of this software is granted only if the user accepts
|
||||||
|
full responsibility for any undesirable consequences; the authors accept
|
||||||
|
NO LIABILITY for damages of any kind.
|
||||||
|
|
||||||
|
These conditions apply to any software derived from or based on the IJG code,
|
||||||
|
not just to the unmodified library. If you use our work, you ought to
|
||||||
|
acknowledge us.
|
||||||
|
|
||||||
|
Permission is NOT granted for the use of any IJG author's name or company name
|
||||||
|
in advertising or publicity relating to this software or products derived from
|
||||||
|
it. This software may be referred to only as "the Independent JPEG Group's
|
||||||
|
software".
|
||||||
|
|
||||||
|
We specifically permit and encourage the use of this software as the basis of
|
||||||
|
commercial products, provided that all warranty or liability claims are
|
||||||
|
assumed by the product vendor.
|
||||||
|
|
||||||
|
|
||||||
|
The Unix configuration script "configure" was produced with GNU Autoconf.
|
||||||
|
It is copyright by the Free Software Foundation but is freely distributable.
|
||||||
|
The same holds for its supporting scripts (config.guess, config.sub,
|
||||||
|
ltmain.sh). Another support script, install-sh, is copyright by X Consortium
|
||||||
|
but is also freely distributable.
|
||||||
|
|
||||||
|
The IJG distribution formerly included code to read and write GIF files.
|
||||||
|
To avoid entanglement with the Unisys LZW patent, GIF reading support has
|
||||||
|
been removed altogether, and the GIF writer has been simplified to produce
|
||||||
|
"uncompressed GIFs". This technique does not use the LZW algorithm; the
|
||||||
|
resulting GIF files are larger than usual, but are readable by all standard
|
||||||
|
GIF decoders.
|
||||||
|
|
||||||
|
We are required to state that
|
||||||
|
"The Graphics Interchange Format(c) is the Copyright property of
|
||||||
|
CompuServe Incorporated. GIF(sm) is a Service Mark property of
|
||||||
|
CompuServe Incorporated."
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Files: contrib/gdk-pixbuf/*
|
||||||
|
|
||||||
|
Copyright 2020 Emmanuel Gil Peyrot. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Files: android_jni/gradlew*
|
||||||
|
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Files: third_party/libyuv/*
|
||||||
|
|
||||||
|
Copyright 2011 The LibYuv Project Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
* Neither the name of Google nor the names of its contributors may
|
||||||
|
be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29
wheels/dependency_licenses/LIBYUV.txt
Normal file
29
wheels/dependency_licenses/LIBYUV.txt
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
Copyright 2011 The LibYuv Project Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
* Neither the name of Google nor the names of its contributors may
|
||||||
|
be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user