Merge branch 'main' into image_grab_wayland_kde

This commit is contained in:
Adian Kozlica 2025-04-01 09:43:52 +02:00 committed by GitHub
commit 2b62c0beeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
104 changed files with 4278 additions and 50 deletions

View File

@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev
sway wl-clipboard libopenblas-dev nasm
fi
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-timeout
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
python3 -m pip install numpy
@ -62,6 +65,9 @@ if [[ $(uname) != CYGWIN* ]]; then
# raqm
pushd depends && ./install_raqm.sh && popd
# libavif
pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
else

View File

@ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \
aom \
dav1d \
freetype \
ghostscript \
jpeg-turbo \
@ -14,6 +16,8 @@ brew install \
libtiff \
little-cms2 \
openjpeg \
rav1e \
svt-av1 \
webp
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 pyroma
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
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -60,6 +60,7 @@ jobs:
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-ghostscript \
mingw-w64-x86_64-lcms2 \
mingw-w64-x86_64-libavif \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libjpeg-turbo \
mingw-w64-x86_64-libraqm \

View File

@ -42,7 +42,7 @@ jobs:
# Test the oldest Python on 32-bit
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
timeout-minutes: 30
timeout-minutes: 45
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
@ -88,6 +88,10 @@ jobs:
run: |
python3 -m pip install PyQt6
- name: Install PyArrow dependency
run: |
python3 -m pip install --only-binary=:all: pyarrow || true
- name: Install dependencies
id: install
run: |
@ -145,6 +149,10 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true'
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
- name: Build dependencies / brotli
if: steps.build-cache.outputs.cache-hit != 'true'

View File

@ -25,7 +25,7 @@ else
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
MB_ML_VER=${AUDITWHEEL_POLICY:9}
fi
PLAT=$CIBW_ARCHS
PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}"
# Define custom utilities
source wheels/multibuild/common_utils.sh
@ -42,18 +42,30 @@ HARFBUZZ_VERSION=11.0.0
LIBPNG_VERSION=1.6.47
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
if [[ $MB_ML_VER == 2014 ]]; then
XZ_VERSION=5.6.4
else
XZ_VERSION=5.8.0
fi
XZ_VERSION=5.8.0
TIFF_VERSION=4.7.0
LCMS2_VERSION=2.17
ZLIB_VERSION=1.3.1
ZLIB_NG_VERSION=2.2.4
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.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 {
if [ -e pkg-config-stamp ]; then return; fi
@ -105,12 +117,55 @@ function build_harfbuzz {
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 {
build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel
fi
build_zlib_ng
if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then
build_new_zlib
else
build_zlib_ng
fi
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
@ -135,6 +190,7 @@ function build {
build_tiff
fi
build_libavif
build_libpng
build_lcms2
build_openjpeg

View File

@ -160,6 +160,11 @@ jobs:
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
shell: pwsh
- name: Update rust
if: matrix.cibw_arch == 'AMD64'
run: |
rustup update
- name: Build wheels
run: |
setlocal EnableDelayedExpansion

View File

@ -1,12 +1,16 @@
from __future__ import annotations
import platform
import struct
import sys
from PIL import features
from .helper import is_pypy
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
try:
@ -16,6 +20,11 @@ def test_wheel_modules() -> None:
except ImportError:
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
@ -40,5 +49,7 @@ def test_wheel_features() -> None:
if sys.platform == "win32":
expected_features.remove("xcb")
elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm":
expected_features.remove("zlib_ng")
assert set(features.get_supported_features()) == expected_features

BIN
Tests/images/avif/exif.avif Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

164
Tests/test_arrow.py Normal file
View 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)

View File

@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
# (referenced from https://wiki.mozilla.org/APNG_Specification)
def test_apng_basic() -> None:
with Image.open("Tests/images/apng/single_frame.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
assert im.n_frames == 1
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)
with Image.open("Tests/images/apng/single_frame_default.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.is_animated
assert im.n_frames == 2
assert im.get_format_mimetype() == "image/apng"
@ -52,6 +54,7 @@ def test_apng_basic() -> None:
)
def test_apng_fdat(filename: str) -> None:
with Image.open(filename) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
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:
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 97)
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:
with Image.open("Tests/images/apng/fctl_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (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:
with Image.open("Tests/images/apng/mode_16bit.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "RGBA"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "L"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == 128
assert im.getpixel((64, 32)) == 255
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "LA"
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (128, 191)
assert im.getpixel((64, 32)) == (128, 191)
with Image.open("Tests/images/apng/mode_palette.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGB")
@ -258,6 +282,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0)
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
@ -265,6 +290,7 @@ def test_apng_mode() -> None:
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
@ -274,25 +300,31 @@ def test_apng_mode() -> None:
def test_apng_chunk_errors() -> None:
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
im.load()
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1)
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(SyntaxError):
im.seek(im.n_frames - 1)
@ -300,26 +332,31 @@ def test_apng_chunk_errors() -> None:
def test_apng_syntax_errors() -> None:
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
with pytest.raises(OSError):
im.load()
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
im.load()
# we can handle this case gracefully
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
with pytest.raises(OSError):
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.load()
with pytest.warns(UserWarning):
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert not im.is_animated
im.load()
@ -339,6 +376,7 @@ def test_apng_syntax_errors() -> None:
def test_apng_sequence_errors(test_file: str) -> None:
with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{test_file}") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
im.load()
@ -349,6 +387,7 @@ def test_apng_save(tmp_path: Path) -> None:
im.save(test_file, save_all=True)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load()
assert not im.is_animated
assert im.n_frames == 1
@ -364,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None:
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.load()
assert im.is_animated
assert im.n_frames == 2
@ -403,6 +443,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
append_images=frames,
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
im.seek(im.n_frames - 1)
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]
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 1
assert "duration" not in im.info
@ -456,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
duration=[500, 100, 150],
)
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2
assert im.info["duration"] == 600
@ -466,6 +509,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
frame.info["duration"] = 300
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
with Image.open(test_file) as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.n_frames == 2
assert im.info["duration"] == 600

778
Tests/test_file_avif.py Normal file
View 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)

View File

@ -69,12 +69,14 @@ def test_tell() -> None:
def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, DcxImagePlugin.DcxImageFile)
assert im.n_frames == 1
assert not im.is_animated
def test_eoferror() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, DcxImagePlugin.DcxImageFile)
n_frames = im.n_frames
# Test seeking past the last frame

View File

@ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = (
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image:
assert isinstance(image, EpsImagePlugin.EpsImageFile)
image.load(scale=scale)
assert image.mode == "RGB"
assert image.size == expected_size
@ -227,6 +229,8 @@ def test_showpage() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_transparency() -> None:
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
assert isinstance(plot_image, EpsImagePlugin.EpsImageFile)
plot_image.load(transparency=True)
assert plot_image.mode == "RGBA"
@ -308,6 +312,7 @@ def test_render_scale2() -> None:
# Zero bounding box
with Image.open(FILE1) as image1_scale2:
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
image1_scale2.load(scale=2)
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
image1_scale2_compare = image1_scale2_compare.convert("RGB")
@ -316,6 +321,7 @@ def test_render_scale2() -> None:
# Non-zero bounding box
with Image.open(FILE2) as image2_scale2:
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
image2_scale2.load(scale=2)
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
image2_scale2_compare = image2_scale2_compare.convert("RGB")

View File

@ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
def test_sanity() -> None:
with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.load()
assert im.mode == "P"
assert im.size == (128, 128)
@ -29,6 +31,8 @@ def test_sanity() -> None:
assert not im.is_animated
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.mode == "P"
assert im.size == (320, 200)
assert im.format == "FLI"
@ -112,16 +116,19 @@ def test_palette_chunk_second() -> None:
def test_n_frames() -> None:
with Image.open(static_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 1
assert not im.is_animated
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
assert im.n_frames == 384
assert im.is_animated
def test_eoferror() -> None:
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -166,6 +173,7 @@ def test_seek_tell() -> None:
def test_seek() -> None:
with Image.open(animated_test_file) as im:
assert isinstance(im, FliImagePlugin.FliImageFile)
im.seek(50)
assert_image_equal_tofile(im, "Tests/images/a_fli.png")

View File

@ -22,10 +22,11 @@ def test_sanity() -> None:
def test_close() -> None:
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
pass
assert isinstance(im, FpxImagePlugin.FpxImageFile)
assert im.ole.fp.closed
im = Image.open("Tests/images/input_bw_one_band.fpx")
assert isinstance(im, FpxImagePlugin.FpxImageFile)
im.close()
assert im.ole.fp.closed

View File

@ -402,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
def test_seek() -> None:
with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
frame_count = 0
try:
while True:
@ -446,10 +447,12 @@ def test_seek_rewind() -> None:
def test_n_frames(path: str, n_frames: int) -> None:
# Test is_animated before n_frames
with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames
with Image.open(path) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == n_frames
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:
im.seek(1)
expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.n_frames == 5
assert_image_equal(im, expected)
@ -466,17 +470,20 @@ def test_no_change() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as im:
im.seek(3)
expected = im.copy()
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.is_animated
assert_image_equal(im, expected)
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
expected = Image.new("P", (1, 1))
assert isinstance(im, GifImagePlugin.GifImageFile)
assert not im.is_animated
assert_image_equal(im, expected)
def test_eoferror() -> None:
with Image.open(TEST_GIF) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -495,6 +502,7 @@ def test_first_frame_transparency() -> None:
def test_dispose_none() -> None:
with Image.open("Tests/images/dispose_none.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try:
while True:
img.seek(img.tell() + 1)
@ -518,6 +526,7 @@ def test_dispose_none_load_end() -> None:
def test_dispose_background() -> None:
with Image.open("Tests/images/dispose_bgnd.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try:
while True:
img.seek(img.tell() + 1)
@ -571,6 +580,7 @@ def test_transparent_dispose(
def test_dispose_previous() -> None:
with Image.open("Tests/images/dispose_prev.gif") as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
try:
while True:
img.seek(img.tell() + 1)
@ -608,6 +618,7 @@ def test_save_dispose(tmp_path: Path) -> None:
for method in range(4):
im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)
with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for _ in range(2):
img.seek(img.tell() + 1)
assert img.disposal_method == method
@ -621,6 +632,7 @@ def test_save_dispose(tmp_path: Path) -> None:
)
with Image.open(out) as img:
assert isinstance(img, GifImagePlugin.GifImageFile)
for i in range(2):
img.seek(img.tell() + 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)
with Image.open(out) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
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
)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that the first three frames were combined
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)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
# Assert that all frames were combined
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)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3
# Test append_images without save_all
im.copy().save(out, append_images=ims)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3
# 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))
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 3
# 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])
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
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)
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
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])
with Image.open(out) as reread:
assert isinstance(reread, GifImagePlugin.GifImageFile)
assert reread.n_frames == 2
@ -1425,6 +1448,7 @@ def test_extents(
) -> None:
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
with Image.open("Tests/images/" + test_file) as im:
assert isinstance(im, GifImagePlugin.GifImageFile)
assert im.size == (100, 100)
# 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)
with Image.open(out) as reloaded:
assert isinstance(reloaded, GifImagePlugin.GifImageFile)
assert reloaded.n_frames == 2

View File

@ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None:
assert_image_similar_tofile(im, temp_file, 1)
with Image.open(temp_file) as reread:
assert isinstance(reread, IcnsImagePlugin.IcnsImageFile)
reread.size = (16, 16)
reread.load(2)
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
# dimensions are as expected
with Image.open(TEST_FILE) as im:
assert isinstance(im, IcnsImagePlugin.IcnsImageFile)
for w, h, r in im.info["sizes"]:
wr = w * r
hr = h * r
@ -118,6 +120,7 @@ def test_older_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2:
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
@ -135,6 +138,7 @@ def test_jp2_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2:
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"

View File

@ -77,6 +77,7 @@ def test_save_to_bytes() -> None:
# The other one
output.seek(0)
with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32)
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)])
with Image.open(temp_file) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.load()
reloaded.size = (32, 32)
@ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
# The other one
output.seek(0)
with Image.open(output) as reloaded:
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
reloaded.size = (32, 32)
assert "RGBA" == reloaded.mode
@ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
def test_incorrect_size() -> None:
with Image.open(TEST_ICO_FILE) as im:
assert isinstance(im, IcoImagePlugin.IcoImageFile)
with pytest.raises(ValueError):
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])
with Image.open(outfile) as reread:
assert isinstance(reread, IcoImagePlugin.IcoImageFile)
assert_image_equal(reread, hopper("RGBA"))
reread.size = (32, 32)

View File

@ -68,12 +68,14 @@ def test_tell() -> None:
def test_n_frames() -> None:
with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
assert im.n_frames == 1
assert not im.is_animated
def test_eoferror() -> None:
with Image.open(TEST_IM) as im:
assert isinstance(im, ImImagePlugin.ImImageFile)
n_frames = im.n_frames
# Test seeking past the last frame

View File

@ -91,6 +91,7 @@ class TestFileJpeg:
def test_app(self) -> None:
# Test APP/COM reader (@PIL135)
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[1] == (
"COM",
@ -316,6 +317,8 @@ class TestFileJpeg:
def test_exif_typeerror(self) -> None:
with Image.open("Tests/images/exif_typeerror.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
# Should not raise a TypeError
im._getexif()
@ -500,6 +503,7 @@ class TestFileJpeg:
def test_mp(self) -> None:
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert im._getmp() is None
def test_quality_keep(self, tmp_path: Path) -> None:
@ -558,12 +562,14 @@ class TestFileJpeg:
with Image.open(test_file) as im:
im.save(b, "JPEG", qtables=[[n] * 64] * n)
with Image.open(b) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert len(im.quantization) == n
reloaded = self.roundtrip(im, qtables="keep")
assert im.quantization == reloaded.quantization
assert max(reloaded.quantization[0]) <= 255
with Image.open("Tests/images/hopper.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
qtables = im.quantization
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
assert im.quantization == reloaded.quantization
@ -663,6 +669,7 @@ class TestFileJpeg:
def test_load_16bit_qtables(self) -> None:
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[0]) == 64
assert max(im.quantization[0]) > 255
@ -705,6 +712,7 @@ class TestFileJpeg:
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg(self) -> None:
with Image.open(TEST_FILE) as img:
assert isinstance(img, JpegImagePlugin.JpegImageFile)
img.load_djpeg()
assert_image_similar_tofile(img, TEST_FILE, 5)
@ -909,6 +917,7 @@ class TestFileJpeg:
def test_photoshop_malformed_and_multiple(self) -> None:
with Image.open("Tests/images/app13-multiple.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
assert "photoshop" in im.info
assert 24 == len(im.info["photoshop"])
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:
with Image.open(TEST_FILE) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
with pytest.warns(DeprecationWarning):
assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning):

View File

@ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None:
out.seek(0)
with Image.open(out) as im:
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
im.layers = 1
im.load()
assert_image_similar(im, card, 13)
out.seek(0)
with Image.open(out) as im:
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
im.layers = 3
im.load()
assert_image_similar(im, card, 0.4)

View File

@ -36,6 +36,7 @@ class LibTiffTestCase:
im.load()
im.getdata()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._compression == "group4"
# can we write it back out, in a different form.
@ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase):
"""Test metadata writing through libtiff"""
f = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper_g4.tif") as img:
assert isinstance(img, TiffImagePlugin.TiffImageFile)
img.save(f, tiffinfo=img.tag)
if legacy_api:
@ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase):
]
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
if legacy_api:
reloaded = loaded.tag.named()
else:
@ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Exclude ones that have special meaning
# that we're already testing them
with Image.open("Tests/images/hopper_g4.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
for tag in im.tag_v2:
try:
del core_items[tag]
@ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, tiffinfo=tiffinfo)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
if (
@ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase):
def test_osubfiletype(self, tmp_path: Path) -> None:
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[OSUBFILETYPE] = 1
im.save(outfile)
def test_subifd(self, tmp_path: Path) -> None:
outfile = tmp_path / "temp.tif"
with Image.open("Tests/images/g4_orientation_6.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.tag_v2[SUBIFD] = 10000
# Should not segfault
@ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase):
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag"
@ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase):
"""Tests String data in info directory"""
test_file = "Tests/images/hopper_g4_500.tif"
with Image.open(test_file) as orig:
assert isinstance(orig, TiffImagePlugin.TiffImageFile)
out = tmp_path / "temp.tif"
orig.tag[269] = "temp.tif"
orig.save(out)
with Image.open(out) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert "temp.tif" == reread.tag_v2[269]
assert "temp.tif" == reread.tag[269][0]
@ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as reloaded:
# colormap/palette tag
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert len(reloaded.tag_v2[320]) == 768
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
@ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/multipage.tiff") as im:
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.seek(0)
assert im.size == (10, 10)
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
@ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase):
# issue #862
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
frames = im.n_frames
assert frames == 3
for _ in range(frames):
@ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert not im.tag.next
im.load()
assert not im.tag.next
@ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(outfile, compression="jpeg")
with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self) -> None:
out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[34665] == 125456
im.save(out, "TIFF")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 34665 not in reloaded.tag_v2
im.save(out, "TIFF", tiffinfo={34665: 125456})
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456
@ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.seek(0)
assert im._compression == "tiff_ccitt"
assert im.size == (10, 10)
@ -1090,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 274 in im.tag_v2
im.load()

View File

@ -30,11 +30,13 @@ def test_sanity() -> None:
def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, MicImagePlugin.MicImageFile)
assert im.n_frames == 1
def test_is_animated() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, MicImagePlugin.MicImageFile)
assert not im.is_animated
@ -55,10 +57,11 @@ def test_seek() -> None:
def test_close() -> None:
with Image.open(TEST_FILE) as im:
pass
assert isinstance(im, MicImagePlugin.MicImageFile)
assert im.ole.fp.closed
im = Image.open(TEST_FILE)
assert isinstance(im, MicImagePlugin.MicImageFile)
im.close()
assert im.ole.fp.closed

View File

@ -6,7 +6,7 @@ from typing import Any
import pytest
from PIL import Image, ImageFile, MpoImagePlugin
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
from .helper import (
assert_image_equal,
@ -80,6 +80,7 @@ def test_context_manager() -> None:
def test_app(test_file: str) -> None:
# Test APP/COM reader (@PIL135)
with Image.open(test_file) as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.applist[0][0] == "APP1"
assert im.applist[1][0] == "APP2"
assert im.applist[1][1].startswith(
@ -220,12 +221,14 @@ def test_seek(test_file: str) -> None:
def test_n_frames() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
assert im.n_frames == 2
assert im.is_animated
def test_eoferror() -> None:
with Image.open("Tests/images/sugarshack.mpo") as im:
assert isinstance(im, MpoImagePlugin.MpoImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -239,6 +242,8 @@ def test_eoferror() -> None:
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)

View File

@ -576,6 +576,7 @@ class TestFilePng:
def test_read_private_chunks(self) -> None:
with Image.open("Tests/images/exif.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert im.private_chunks == [(b"orNT", b"\x01")]
def test_roundtrip_private_chunk(self) -> None:
@ -598,6 +599,7 @@ class TestFilePng:
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
assert "comment" in im.text
for k, v in {
"date:create": "2014-09-04T09:37:08+03:00",
@ -607,15 +609,19 @@ class TestFilePng:
# Raises a SyntaxError in load_end
with Image.open("Tests/images/broken_data_stream.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
with pytest.raises(OSError):
assert isinstance(im.text, dict)
# Raises an EOFError in load_end
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"}
# Raises a UnicodeDecodeError in load_end
with Image.open("Tests/images/truncated_image.png") as im:
assert isinstance(im, PngImagePlugin.PngImageFile)
# The file is truncated
with pytest.raises(OSError):
im.text
@ -726,6 +732,7 @@ class TestFilePng:
im.save(test_file)
with Image.open(test_file) as reloaded:
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
assert reloaded._getexif() is None
# Test passing in exif

View File

@ -59,17 +59,21 @@ def test_invalid_file() -> None:
def test_n_frames() -> None:
with Image.open("Tests/images/hopper_merged.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 1
assert not im.is_animated
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
with Image.open(path) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
assert im.n_frames == 2
assert im.is_animated
def test_eoferror() -> None:
with Image.open(test_file) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
# PSD seek index starts at 1 rather than 0
n_frames = im.n_frames + 1
@ -119,11 +123,13 @@ def test_rgba() -> None:
def test_negative_top_left_layer() -> None:
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)
def test_layer_skip() -> None:
with Image.open("Tests/images/five_channels.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
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:
with open(test_file, "rb") as f:
with Image.open(f) as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
with pytest.raises(SyntaxError):
im.layers

View File

@ -96,6 +96,7 @@ def test_tell() -> None:
def test_n_frames() -> None:
with Image.open(TEST_FILE) as im:
assert isinstance(im, SpiderImagePlugin.SpiderImageFile)
assert im.n_frames == 1
assert not im.is_animated

View File

@ -9,7 +9,13 @@ from types import ModuleType
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 .helper import (
@ -113,6 +119,7 @@ class TestFileTiff:
with Image.open("Tests/images/hopper_bigtiff.tif") as im:
outfile = tmp_path / "temp.tif"
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
def test_bigtiff_save(self, tmp_path: Path) -> None:
@ -121,11 +128,13 @@ class TestFileTiff:
im.save(outfile, big_tiff=True)
with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2._bigtiff is True
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
with Image.open(outfile) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2._bigtiff is True
def test_seek_too_large(self) -> None:
@ -140,6 +149,8 @@ class TestFileTiff:
def test_xyres_tiff(self) -> None:
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# legacy api
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
@ -153,6 +164,8 @@ class TestFileTiff:
def test_xyres_fallback_tiff(self) -> None:
filename = "Tests/images/compression.tif"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# v2 api
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
@ -167,6 +180,8 @@ class TestFileTiff:
def test_int_resolution(self) -> None:
filename = "Tests/images/pil168.tif"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# Try to read a file where X,Y_RESOLUTION are ints
im.tag_v2[X_RESOLUTION] = 71
im.tag_v2[Y_RESOLUTION] = 71
@ -181,6 +196,7 @@ class TestFileTiff:
with Image.open(
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
assert im.info["dpi"] == (dpi, dpi)
@ -198,6 +214,7 @@ class TestFileTiff:
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
im.save(b, format="tiff", resolution=123.45)
with Image.open(b) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.tag_v2[X_RESOLUTION] == 123.45
assert im.tag_v2[Y_RESOLUTION] == 123.45
@ -213,10 +230,12 @@ class TestFileTiff:
TiffImagePlugin.PREFIXES.pop()
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.
with pytest.warns(UserWarning):
i._getexif()
im._getexif()
def test_save_rgba(self, tmp_path: Path) -> None:
im = hopper("RGBA")
@ -307,11 +326,13 @@ class TestFileTiff:
)
def test_n_frames(self, path: str, n_frames: int) -> None:
with Image.open(path) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
def test_eoferror(self) -> None:
with Image.open("Tests/images/multipage-lastframe.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
n_frames = im.n_frames
# Test seeking past the last frame
@ -355,19 +376,24 @@ class TestFileTiff:
def test_frame_order(self) -> None:
# A frame can't progress to itself after reading
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 1
# 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:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 2
# Frames don't have to be in sequence
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 3
def test___str__(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# Act
ret = str(im.ifd)
@ -378,6 +404,8 @@ class TestFileTiff:
# Arrange
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# v2 interface
v2_tags = {
256: 55,
@ -417,6 +445,7 @@ class TestFileTiff:
def test__delitem__(self) -> None:
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
len_before = len(dict(im.ifd))
del im.ifd[256]
len_after = len(dict(im.ifd))
@ -449,6 +478,7 @@ class TestFileTiff:
def test_ifd_tag_type(self) -> None:
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 0x8825 in im.tag_v2
def test_exif(self, tmp_path: Path) -> None:
@ -537,6 +567,7 @@ class TestFileTiff:
im = hopper(mode)
im.save(filename, tiffinfo={262: 0})
with Image.open(filename) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[262] == 0
assert_image_equal(im, reloaded)
@ -615,6 +646,8 @@ class TestFileTiff:
filename = tmp_path / "temp.tif"
hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
assert im.tag[Y_RESOLUTION][0][0] == 36
@ -701,6 +734,7 @@ class TestFileTiff:
def test_planar_configuration_save(self, tmp_path: Path) -> None:
infile = "Tests/images/tiff_tiled_planar_raw.tif"
with Image.open(infile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im._planar_configuration == 2
outfile = tmp_path / "temp.tif"
@ -733,6 +767,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert im.n_frames == 3
# Test appending images
@ -743,6 +778,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert reread.n_frames == 3
# Test appending using a generator
@ -754,6 +790,7 @@ class TestFileTiff:
mp.seek(0, os.SEEK_SET)
with Image.open(mp) as reread:
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
assert reread.n_frames == 3
def test_fixoffsets(self) -> None:
@ -864,6 +901,7 @@ class TestFileTiff:
def test_get_photoshop_blocks(self) -> None:
with Image.open("Tests/images/lab.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert list(im.get_photoshop_blocks().keys()) == [
1061,
1002,

View File

@ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None:
img.save(f, tiffinfo=info)
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
assert loaded.tag[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)
img.save(f, tiffinfo=info)
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
def test_read_metadata() -> None:
with Image.open("Tests/images/hopper_g4.tif") as img:
assert isinstance(img, TiffImagePlugin.TiffImageFile)
assert {
"YResolution": IFDRational(4294967295, 113653537),
"PlanarConfiguration": 1,
@ -128,6 +131,7 @@ def test_read_metadata() -> None:
def test_write_metadata(tmp_path: Path) -> None:
"""Test metadata writing through the python code"""
with Image.open("Tests/images/hopper.tif") as img:
assert isinstance(img, TiffImagePlugin.TiffImageFile)
f = tmp_path / "temp.tiff"
del img.tag[278]
img.save(f, tiffinfo=img.tag)
@ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None:
original = img.tag_v2.named()
with Image.open(f) as loaded:
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
reloaded = loaded.tag_v2.named()
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:
out = tmp_path / "temp.tiff"
with Image.open("Tests/images/hopper.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
info = im.tag_v2
del info[278]
@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
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)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
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)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[700] == b"\x01"
@ -267,6 +276,7 @@ def test_writing_other_types_to_undefined(
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
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.
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.info["icc_profile"]
@ -336,6 +347,7 @@ def test_exif_div_zero(tmp_path: Path) -> None:
im.save(out, tiffinfo=info, compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 0 == reloaded.tag_v2[41988].numerator
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")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert max_long == reloaded.tag_v2[41493].numerator
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")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert max_long == reloaded.tag_v2[41493].numerator
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")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert numerator == reloaded.tag_v2[37380].numerator
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")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert numerator == reloaded.tag_v2[37380].numerator
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")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
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")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert reloaded.tag_v2[37000] == -60000
@ -444,11 +462,13 @@ def test_empty_values() -> None:
def test_photoshop_info(tmp_path: Path) -> None:
with Image.open("Tests/images/issue_2278.tif") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert len(im.tag_v2[34377]) == 70
assert isinstance(im.tag_v2[34377], bytes)
out = tmp_path / "temp.tiff"
im.save(out)
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert len(reloaded.tag_v2[34377]) == 70
assert isinstance(reloaded.tag_v2[34377], bytes)

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest
from packaging.version import parse as parse_version
from PIL import Image, features
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
from .helper import (
assert_image_equal,
@ -22,10 +22,12 @@ def test_n_frames() -> None:
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
with Image.open("Tests/images/hopper.webp") as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 1
assert not im.is_animated
with Image.open("Tests/images/iss634.webp") as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 42
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:
assert isinstance(orig, GifImagePlugin.GifImageFile)
assert orig.n_frames > 1
temp_file = tmp_path / "temp.webp"
orig.save(temp_file, save_all=True)
with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == orig.n_frames
# 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:
with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 2
# 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:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 5
assert im.is_animated
@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None:
)
with Image.open(temp_file) as im:
assert isinstance(im, WebPImagePlugin.WebPImageFile)
assert im.n_frames == 5
assert im.is_animated

View File

@ -6,7 +6,7 @@ from types import ModuleType
import pytest
from PIL import Image
from PIL import Image, WebPImagePlugin
from .helper import mark_if_feature_version, skip_unless_feature
@ -110,6 +110,7 @@ def test_read_no_exif() -> None:
test_buffer.seek(0)
with Image.open(test_buffer) as webp_image:
assert isinstance(webp_image, WebPImagePlugin.WebPImageFile)
assert not webp_image._getexif()

View File

@ -89,6 +89,7 @@ def test_load_float_dpi() -> None:
def test_load_set_dpi() -> None:
with Image.open("Tests/images/drawing.wmf") as im:
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
assert im.size == (82, 82)
if hasattr(Image.core, "drawwmf"):
@ -102,10 +103,12 @@ def test_load_set_dpi() -> None:
if not hasattr(Image.core, "drawwmf"):
return
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
im.load(im.info["dpi"])
assert im.size == (1625, 1625)
with Image.open("Tests/images/drawing.emf") as im:
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
im.load((72, 144))
assert im.size == (82, 164)

View File

@ -30,6 +30,7 @@ def test_invalid_file() -> None:
def test_load_read() -> None:
# Arrange
with Image.open(TEST_FILE) as im:
assert isinstance(im, XpmImagePlugin.XpmImageFile)
dummy_bytes = 1
# Act

View File

@ -230,10 +230,10 @@ class TestImage:
assert_image_similar(im, reloaded, 20)
def test_unknown_extension(self, tmp_path: Path) -> None:
im = hopper()
temp_file = tmp_path / "temp.unknown"
with pytest.raises(ValueError):
im.save(temp_file)
with hopper() as im:
with pytest.raises(ValueError):
im.save(temp_file)
def test_internals(self) -> None:
im = Image.new("L", (100, 100))

View File

@ -4,7 +4,7 @@ from pathlib import Path
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
@ -31,6 +31,7 @@ def test_sanity(tmp_path: Path) -> None:
def test_iterator() -> None:
with Image.open("Tests/images/multipage.tiff") as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
i = ImageSequence.Iterator(im)
for index in range(im.n_frames):
assert i[index] == next(i)
@ -42,6 +43,7 @@ def test_iterator() -> None:
def test_iterator_min_frame() -> None:
with Image.open("Tests/images/hopper.psd") as im:
assert isinstance(im, PsdImagePlugin.PsdImageFile)
i = ImageSequence.Iterator(im)
for index in range(1, im.n_frames):
assert i[index] == next(i)

112
Tests/test_pyarrow.py Normal file
View 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)

View File

@ -39,6 +39,7 @@ class TestShellInjection:
shutil.copy(TEST_JPG, src_file)
with Image.open(src_file) as im:
assert isinstance(im, JpegImagePlugin.JpegImageFile)
im.load_djpeg()
@pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available")

View File

@ -72,4 +72,5 @@ def test_ifd_rational_save(
im.save(out, dpi=(res, res), compression="raw")
with Image.open(out) as reloaded:
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])

64
depends/install_libavif.sh Executable file
View 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

View File

@ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
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
^^^
@ -242,7 +319,7 @@ following options are 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.
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
sizes, they will be used instead of scaling down the main image.

View File

@ -89,6 +89,14 @@ Many of Pillow's features require external libraries:
* **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
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
``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::
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
<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
@ -187,7 +209,8 @@ Many of Pillow's features require external libraries:
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-libimagequant \
mingw-w64-x86_64-libraqm
mingw-w64-x86_64-libraqm \
mingw-w64-x86_64-libavif
.. tab:: FreeBSD
@ -199,7 +222,7 @@ Many of Pillow's features require external libraries:
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.

View File

@ -79,6 +79,7 @@ Constructing images
.. autofunction:: new
.. autofunction:: fromarray
.. autofunction:: fromarrow
.. autofunction:: frombytes
.. autofunction:: frombuffer
@ -370,6 +371,8 @@ Protocols
.. autoclass:: SupportsArrayInterface
:show-inheritance:
.. autoclass:: SupportsArrowArrayInterface
:show-inheritance:
.. autoclass:: SupportsGetData
:show-inheritance:

View 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.

View File

@ -1,3 +1,6 @@
.. _block_allocator:
Block Allocator
===============

View File

@ -21,6 +21,7 @@ Support for the following modules can be checked:
* ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`.
* ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`.
* ``webp``: WebP image support.
* ``avif``: AVIF image support.
.. autofunction:: PIL.features.check_module
.. autofunction:: PIL.features.version_module

View File

@ -9,3 +9,4 @@ Internal Reference
block_allocator
internal_modules
c_extension_debugging
arrow_support

View File

@ -1,6 +1,14 @@
Plugin reference
================
:mod:`~PIL.AvifImagePlugin` Module
----------------------------------
.. automodule:: PIL.AvifImagePlugin
:members:
:undoc-members:
:show-inheritance:
:mod:`~PIL.BmpImagePlugin` Module
---------------------------------

View File

@ -68,3 +68,12 @@ Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1,
DXT5, BC2, BC3 and BC5 are supported::
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.

View File

@ -54,6 +54,10 @@ optional-dependencies.fpx = [
optional-dependencies.mic = [
"olefile",
]
optional-dependencies.test-arrow = [
"pyarrow",
]
optional-dependencies.tests = [
"check-manifest",
"coverage>=7.4.2",
@ -67,6 +71,7 @@ optional-dependencies.tests = [
"pytest-timeout",
"trove-classifiers>=2024.10.12",
]
optional-dependencies.typing = [
"typing-extensions; python_version<'3.10'",
]

View File

@ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {}
PILLOW_VERSION = get_version()
AVIF_ROOT = None
FREETYPE_ROOT = None
HARFBUZZ_ROOT = None
FRIBIDI_ROOT = None
@ -64,6 +65,7 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path")
_LIB_IMAGING = (
"Access",
"AlphaComposite",
"Arrow",
"Resample",
"Reduce",
"Bands",
@ -306,6 +308,7 @@ class pil_build_ext(build_ext):
"jpeg2000",
"imagequant",
"xcb",
"avif",
]
required = {"jpeg", "zlib"}
@ -481,6 +484,7 @@ class pil_build_ext(build_ext):
#
# add configured kits
for root_name, lib_name in {
"AVIF_ROOT": "avif",
"JPEG_ROOT": "libjpeg",
"JPEG2K_ROOT": "libopenjp2",
"TIFF_ROOT": ("libtiff-5", "libtiff-4"),
@ -846,6 +850,12 @@ class pil_build_ext(build_ext):
if _find_library_file(self, "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:
if not feature.get(f) and feature.require(f):
if f in ("jpeg", "zlib"):
@ -934,6 +944,14 @@ class pil_build_ext(build_ext):
else:
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 []
self._update_extension("PIL._imagingtk", tk_libs)
@ -976,6 +994,7 @@ class pil_build_ext(build_ext):
(feature.get("lcms"), "LITTLECMS2"),
(feature.get("webp"), "WEBP"),
(feature.get("xcb"), "XCB (X protocol)"),
(feature.get("avif"), "LIBAVIF"),
]
all = 1
@ -1018,6 +1037,7 @@ ext_modules = [
Extension("PIL._imagingft", ["src/_imagingft.c"]),
Extension("PIL._imagingcms", ["src/_imagingcms.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._imagingmath", ["src/_imagingmath.c"]),
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),

292
src/PIL/AvifImagePlugin.py Normal file
View 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")

View File

@ -31,7 +31,7 @@ import os
import subprocess
from enum import IntEnum
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 (
Image,
@ -47,6 +47,7 @@ from ._binary import o8
from ._binary import o16le as o16
from ._util import DeferredError
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import _imaging
from ._typing import Buffer

View File

@ -41,14 +41,7 @@ import warnings
from collections.abc import Callable, Iterator, MutableMapping, Sequence
from enum import IntEnum
from types import ModuleType
from typing import (
IO,
TYPE_CHECKING,
Any,
Literal,
Protocol,
cast,
)
from typing import IO, Any, Literal, Protocol, cast
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@ -218,6 +211,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# --------------------------------------------------------------------
# Registries
TYPE_CHECKING = False
if TYPE_CHECKING:
import mmap
from xml.etree.ElementTree import Element
@ -583,6 +577,14 @@ class Image:
def mode(self) -> str:
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:
new = Image()
new.im = im
@ -734,6 +736,16 @@ class Image:
new["shape"], new["typestr"] = _conv_type_shape(self)
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]:
im_data = self.tobytes() # load image first
return [self.info, self.mode, self.size, self.getpalette(), im_data]
@ -1526,6 +1538,8 @@ class Image:
# XMP tags
if ExifTags.Base.Orientation not in self._exif:
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:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match:
@ -3205,6 +3219,18 @@ class SupportsArrayInterface(Protocol):
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:
"""
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)
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:
"""Creates an image instance from a QImage image"""
from . import ImageQt

View File

@ -35,7 +35,7 @@ import math
import struct
from collections.abc import Sequence
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 ._deprecate import deprecate
@ -44,6 +44,7 @@ from ._typing import Coords
# experimental access to the outline API
Outline: Callable[[], Image.core._Outline] = Image.core.outline
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont

View File

@ -34,12 +34,13 @@ import itertools
import logging
import os
import struct
from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
from typing import IO, Any, NamedTuple, cast
from . import ExifTags, Image
from ._deprecate import deprecate
from ._util import DeferredError, is_path
TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import StrOrBytesPath

View File

@ -20,8 +20,9 @@ import abc
import functools
from collections.abc import Sequence
from types import ModuleType
from typing import TYPE_CHECKING, Any, Callable, cast
from typing import Any, Callable, cast
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import _imaging
from ._typing import NumpyArray

View File

@ -34,12 +34,13 @@ import warnings
from enum import IntEnum
from io import BytesIO
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 ._typing import StrOrBytesPath
from ._util import DeferredError, is_path
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import ImageFile
from ._imaging import ImagingFont

View File

@ -19,10 +19,11 @@ from __future__ import annotations
import array
from collections.abc import Sequence
from typing import IO, TYPE_CHECKING
from typing import IO
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import Image

View File

@ -19,11 +19,12 @@ from __future__ import annotations
import sys
from io import BytesIO
from typing import TYPE_CHECKING, Any, Callable, Union
from typing import Any, Callable, Union
from . import Image
from ._util import is_path
TYPE_CHECKING = False
if TYPE_CHECKING:
import PyQt6
import PySide6

View File

@ -28,10 +28,11 @@ from __future__ import annotations
import tkinter
from io import BytesIO
from typing import TYPE_CHECKING, Any
from typing import Any
from . import Image, ImageFile
TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import CapsuleType

View File

@ -42,7 +42,7 @@ import subprocess
import sys
import tempfile
import warnings
from typing import IO, TYPE_CHECKING, Any
from typing import IO, Any
from . import Image, ImageFile
from ._binary import i16be as i16
@ -52,6 +52,7 @@ from ._binary import o16be as o16
from ._deprecate import deprecate
from .JpegPresets import presets
TYPE_CHECKING = False
if TYPE_CHECKING:
from .MpoImagePlugin import MpoImageFile

View File

@ -17,10 +17,13 @@
from __future__ import annotations
import sys
from typing import IO, TYPE_CHECKING
from typing import IO
from . import EpsImagePlugin
TYPE_CHECKING = False
##
# Simple PostScript graphics interface.

View File

@ -8,7 +8,7 @@ import os
import re
import time
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
@ -251,6 +251,7 @@ class PdfArray(list[Any]):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
TYPE_CHECKING = False
if TYPE_CHECKING:
_DictBase = collections.UserDict[Union[str, bytes], Any]
else:

View File

@ -40,7 +40,7 @@ import warnings
import zlib
from collections.abc import Callable
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 ._binary import i16be as i16
@ -50,6 +50,7 @@ from ._binary import o16be as o16
from ._binary import o32be as o32
from ._util import DeferredError
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import _imaging

View File

@ -37,11 +37,13 @@ from __future__ import annotations
import os
import struct
import sys
from typing import IO, TYPE_CHECKING, Any, cast
from typing import IO, Any, cast
from . import Image, ImageFile
from ._util import DeferredError
TYPE_CHECKING = False
def isInt(f: Any) -> int:
try:

View File

@ -35,12 +35,16 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
while True:
s = self.fh.read(512)
if len(s) != 512:
self.fh.close()
msg = "unexpected end of tar file"
raise OSError(msg)
name = s[:100].decode("utf-8")
i = name.find("\0")
if i == 0:
self.fh.close()
msg = "cannot find subfile"
raise OSError(msg)
if i > 0:

View File

@ -50,7 +50,7 @@ import warnings
from collections.abc import Iterator, MutableMapping
from fractions import Fraction
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 ._binary import i16be as i16
@ -61,6 +61,7 @@ from ._typing import StrOrBytesPath
from ._util import DeferredError, is_path
from .TiffTags import TYPES
TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import Buffer, IntegralLike

View File

@ -25,6 +25,7 @@ del _version
_plugins = [
"AvifImagePlugin",
"BlpImagePlugin",
"BmpImagePlugin",
"BufrStubImagePlugin",

3
src/PIL/_avif.pyi Normal file
View File

@ -0,0 +1,3 @@
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -3,8 +3,9 @@ from __future__ import annotations
import os
import sys
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:
from numbers import _IntegralLike as IntegralLike

View File

@ -17,6 +17,7 @@ modules = {
"freetype2": ("PIL._imagingft", "freetype2_version"),
"littlecms2": ("PIL._imagingcms", "littlecms_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"),
("littlecms2", "LITTLECMS2"),
("webp", "WEBP"),
("avif", "AVIF"),
("jpg", "JPEG"),
("jpg_2000", "OPENJPEG (JPEG2000)"),
("zlib", "ZLIB (PNG/ZIP)"),

908
src/_avif.c Normal file
View 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;
}

View File

@ -230,6 +230,93 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) {
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 */
/* -------------------------------------------------------------------- */
@ -3655,6 +3742,10 @@ static struct PyMethodDef methods[] = {
/* Misc. */
{"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 */
};
@ -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[] = {
{"mode", (getter)_getattr_mode},
{"size", (getter)_getattr_size},
@ -3729,6 +3825,7 @@ static struct PyGetSetDef getsetters[] = {
{"id", (getter)_getattr_id},
{"ptr", (getter)_getattr_ptr},
{"unsafe_ptrs", (getter)_getattr_unsafe_ptrs},
{"readonly", (getter)_getattr_readonly},
{NULL}
};
@ -3983,6 +4080,21 @@ _set_blocks_max(PyObject *self, PyObject *args) {
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 *
_clear_cache(PyObject *self, PyObject *args) {
int i = 0;
@ -4104,6 +4216,7 @@ static PyMethodDef functions[] = {
{"fill", (PyCFunction)_fill, METH_VARARGS},
{"new", (PyCFunction)_new, METH_VARARGS},
{"new_block", (PyCFunction)_new_block, METH_VARARGS},
{"new_arrow", (PyCFunction)_new_arrow, METH_VARARGS},
{"merge", (PyCFunction)_merge, METH_VARARGS},
/* Functions */
@ -4190,9 +4303,11 @@ static PyMethodDef functions[] = {
{"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS},
{"get_block_size", (PyCFunction)_get_block_size, 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_block_size", (PyCFunction)_set_block_size, 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},
{NULL, NULL} /* sentinel */

299
src/libImaging/Arrow.c Normal file
View 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
View 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

View File

@ -20,6 +20,8 @@ extern "C" {
#define M_PI 3.1415926535897932384626433832795
#endif
#include "Arrow.h"
/* -------------------------------------------------------------------- */
/*
@ -104,6 +106,21 @@ struct ImagingMemoryInstance {
/* Virtual methods */
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)])
@ -161,6 +178,7 @@ typedef struct ImagingMemoryArena {
int stats_reallocated_blocks; /* Number of blocks which were actually reallocated
after retrieving */
int stats_freed_blocks; /* Number of freed blocks */
int use_block_allocator; /* don't use arena, use block allocator */
#ifdef Py_GIL_DISABLED
PyMutex mutex;
#endif
@ -174,6 +192,8 @@ extern int
ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max);
extern void
ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size);
extern void
ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator);
extern Imaging
ImagingNew(const char *mode, int xsize, int ysize);
@ -187,6 +207,15 @@ ImagingDelete(Imaging im);
extern Imaging
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
ImagingNewPrologue(const char *mode, int xsize, int ysize);
extern Imaging
@ -700,6 +729,13 @@ _imaging_seek_pyFd(PyObject *fd, Py_ssize_t offset, int whence);
extern Py_ssize_t
_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 */
#define IMAGING_CODEC_END 1
#define IMAGING_CODEC_OVERRUN -1
@ -707,6 +743,8 @@ _imaging_tell_pyFd(PyObject *fd);
#define IMAGING_CODEC_UNKNOWN -3
#define IMAGING_CODEC_CONFIG -8
#define IMAGING_CODEC_MEMORY -9
#define IMAGING_ARROW_INCOMPATIBLE_MODE -10
#define IMAGING_ARROW_MEMORY_LAYOUT -11
#include "ImagingUtils.h"
extern UINT8 *clip8_lookups;

View File

@ -58,19 +58,22 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
/* Setup image descriptor */
im->xsize = xsize;
im->ysize = ysize;
im->refcount = 1;
im->type = IMAGING_TYPE_UINT8;
strcpy(im->arrow_band_format, "C");
if (strcmp(mode, "1") == 0) {
/* 1-bit images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
strcpy(im->band_names[0], "1");
} else if (strcmp(mode, "P") == 0) {
/* 8-bit palette mapped images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
im->palette = ImagingPaletteNew("RGB");
strcpy(im->band_names[0], "P");
} else if (strcmp(mode, "PA") == 0) {
/* 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->linesize = xsize * 4;
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) {
/* 8-bit grayscale (luminance) images */
im->bands = im->pixelsize = 1;
im->linesize = xsize;
strcpy(im->band_names[0], "L");
} else if (strcmp(mode, "LA") == 0) {
/* 8-bit grayscale (luminance) with alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
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) {
/* 8-bit grayscale (luminance) with premultiplied alpha */
im->bands = 2;
im->pixelsize = 4; /* store in image32 memory */
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) {
/* 32-bit floating point images */
@ -102,6 +118,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 4;
im->linesize = xsize * 4;
im->type = IMAGING_TYPE_FLOAT32;
strcpy(im->arrow_band_format, "f");
strcpy(im->band_names[0], "F");
} else if (strcmp(mode, "I") == 0) {
/* 32-bit integer images */
@ -109,6 +127,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 4;
im->linesize = xsize * 4;
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 ||
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->linesize = xsize * 2;
im->type = IMAGING_TYPE_SPECIAL;
strcpy(im->arrow_band_format, "s");
strcpy(im->band_names[0], "I");
} else if (strcmp(mode, "RGB") == 0) {
/* 24-bit true colour images */
im->bands = 3;
im->pixelsize = 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) {
/* EXPERIMENTAL */
@ -132,6 +158,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 2;
im->linesize = (xsize * 2 + 3) & -4;
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) {
/* EXPERIMENTAL */
@ -140,6 +168,8 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 2;
im->linesize = (xsize * 2 + 3) & -4;
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) {
/* EXPERIMENTAL */
@ -148,32 +178,54 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
im->pixelsize = 3;
im->linesize = (xsize * 3 + 3) & -4;
im->type = IMAGING_TYPE_SPECIAL;
/* not allowing arrow due to line length packing */
strcpy(im->arrow_band_format, "");
} else if (strcmp(mode, "RGBX") == 0) {
/* 32-bit true colour images with padding */
im->bands = im->pixelsize = 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) {
/* 32-bit true colour images with alpha */
im->bands = im->pixelsize = 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) {
/* 32-bit true colour images with premultiplied alpha */
im->bands = im->pixelsize = 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) {
/* 32-bit colour separation */
im->bands = im->pixelsize = 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) {
/* 24-bit video format */
im->bands = 3;
im->pixelsize = 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) {
/* 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->pixelsize = 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) {
/* 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->pixelsize = 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 {
free(im);
@ -218,6 +278,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) {
break;
}
// UNDONE -- not accurate for arrow
MUTEX_LOCK(&ImagingDefaultArena.mutex);
ImagingDefaultArena.stats_new_count += 1;
MUTEX_UNLOCK(&ImagingDefaultArena.mutex);
@ -238,8 +299,18 @@ ImagingDelete(Imaging im) {
return;
}
MUTEX_LOCK(&im->mutex);
im->refcount--;
if (im->refcount > 0) {
MUTEX_UNLOCK(&im->mutex);
return;
}
MUTEX_UNLOCK(&im->mutex);
if (im->palette) {
ImagingPaletteDelete(im->palette);
im->palette = NULL;
}
if (im->destroy) {
@ -270,6 +341,7 @@ struct ImagingMemoryArena ImagingDefaultArena = {
0,
0,
0, // Stats
0, // use_block_allocator
#ifdef Py_GIL_DISABLED
{0},
#endif
@ -302,6 +374,11 @@ ImagingMemorySetBlocksMax(ImagingMemoryArena arena, int blocks_max) {
return 1;
}
void
ImagingMemorySetBlockAllocator(ImagingMemoryArena arena, int use_block_allocator) {
arena->use_block_allocator = use_block_allocator;
}
void
ImagingMemoryClearCache(ImagingMemoryArena arena, int 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) {
lines_per_block = 1;
}
im->lines_per_block = 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",
// im->xsize, im->ysize, aligned_linesize, lines_per_block, blocks_count);
/* One extra pointer is always NULL */
im->blocks_count = blocks_count;
im->blocks = calloc(sizeof(*im->blocks), blocks_count + 1);
if (!im->blocks) {
return (Imaging)ImagingError_MemoryError();
@ -487,6 +566,58 @@ ImagingAllocateBlock(Imaging 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.
*/
@ -529,11 +660,17 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) {
Imaging
ImagingNew(const char *mode, int xsize, int ysize) {
if (ImagingDefaultArena.use_block_allocator) {
return ImagingNewBlock(mode, xsize, ysize);
}
return ImagingNewInternal(mode, xsize, ysize, 0);
}
Imaging
ImagingNewDirty(const char *mode, int xsize, int ysize) {
if (ImagingDefaultArena.use_block_allocator) {
return ImagingNewBlock(mode, xsize, ysize);
}
return ImagingNewInternal(mode, xsize, ysize, 1);
}
@ -558,6 +695,66 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) {
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
ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) {
/* allocate or validate output image */

View 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.

View 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.

View 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.

View 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