mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-11-01 00:17:27 +03:00 
			
		
		
		
	Merge branch 'main' into context_manager
This commit is contained in:
		
						commit
						beab3fae1e
					
				|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										10
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then | |||
|     brew uninstall gradle maven | ||||
| 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 | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -60,6 +60,7 @@ jobs: | |||
|               mingw-w64-x86_64-gcc \ | ||||
|               mingw-w64-x86_64-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 \ | ||||
|  |  | |||
							
								
								
									
										10
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -42,7 +42,7 @@ jobs: | |||
|             # Test the oldest Python on 32-bit | ||||
|             - { 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' | ||||
|  |  | |||
							
								
								
									
										76
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										76
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -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 | ||||
|  | @ -68,11 +80,7 @@ function build_pkg_config { | |||
| 
 | ||||
| function build_zlib_ng { | ||||
|     if [ -e zlib-stamp ]; then return; fi | ||||
|     fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz | ||||
|     (cd zlib-ng-$ZLIB_NG_VERSION \ | ||||
|         && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ | ||||
|         && make -j4 \ | ||||
|         && make install) | ||||
|     build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat | ||||
| 
 | ||||
|     if [ -n "$IS_MACOS" ]; then | ||||
|         # Ensure that on macOS, the library name is an absolute path, not an | ||||
|  | @ -105,12 +113,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 +186,7 @@ function build { | |||
|         build_tiff | ||||
|     fi | ||||
| 
 | ||||
|     build_libavif | ||||
|     build_libpng | ||||
|     build_lcms2 | ||||
|     build_openjpeg | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -160,6 +160,11 @@ jobs: | |||
|           & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} | ||||
|         shell: pwsh | ||||
| 
 | ||||
|       - name: Update rust | ||||
|         if: matrix.cibw_arch == 'AMD64' | ||||
|         run: | | ||||
|           rustup update | ||||
| 
 | ||||
|       - name: Build wheels | ||||
|         run: | | ||||
|           setlocal EnableDelayedExpansion | ||||
|  |  | |||
|  | @ -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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/exif.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper-missing-pixi.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper-missing-pixi.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.heif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.heif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper_avif_write.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper_avif_write.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile_none.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile_none.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/star.avifs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/star.avifs
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/star.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/star.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/star.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/star.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/transparency.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/transparency.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/xmp_tags_orientation.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/xmp_tags_orientation.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										164
									
								
								Tests/test_arrow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								Tests/test_arrow.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,164 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import hopper | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, dest_modes", | ||||
|     ( | ||||
|         ("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), | ||||
|         ("I", ["L", "F"]),  # Technically I;32 can work for any 4x8bit storage. | ||||
|         ("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), | ||||
|         ("LA", ["L", "F"]), | ||||
|         ("RGB", ["L", "F"]), | ||||
|         ("RGBA", ["L", "F"]), | ||||
|         ("RGBX", ["L", "F"]), | ||||
|         ("CMYK", ["L", "F"]), | ||||
|         ("YCbCr", ["L", "F"]), | ||||
|         ("HSV", ["L", "F"]), | ||||
|     ), | ||||
| ) | ||||
| def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None: | ||||
|     img = hopper(mode) | ||||
|     for dest_mode in dest_modes: | ||||
|         with pytest.raises(ValueError): | ||||
|             Image.fromarrow(img, dest_mode, img.size) | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_array_size() -> None: | ||||
|     img = hopper("RGB") | ||||
| 
 | ||||
|     assert img.size != (10, 10) | ||||
|     with pytest.raises(ValueError): | ||||
|         Image.fromarrow(img, "RGB", (10, 10)) | ||||
| 
 | ||||
| 
 | ||||
| def test_release_schema() -> None: | ||||
|     # these should not error out, valgrind should be clean | ||||
|     img = hopper("L") | ||||
|     schema = img.__arrow_c_schema__() | ||||
|     del schema | ||||
| 
 | ||||
| 
 | ||||
| def test_release_array() -> None: | ||||
|     # these should not error out, valgrind should be clean | ||||
|     img = hopper("L") | ||||
|     array, schema = img.__arrow_c_array__() | ||||
|     del array | ||||
|     del schema | ||||
| 
 | ||||
| 
 | ||||
| def test_readonly() -> None: | ||||
|     img = hopper("L") | ||||
|     reloaded = Image.fromarrow(img, img.mode, img.size) | ||||
|     assert reloaded.readonly == 1 | ||||
|     reloaded._readonly = 0 | ||||
|     assert reloaded.readonly == 1 | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_l_image() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in single channel mode | ||||
|     size = (4096, 2 * block_size // 4096) | ||||
|     img = Image.new("L", size, 128) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         (schema, arr) = img.__arrow_c_array__() | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_rgba_image() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         (schema, arr) = img.__arrow_c_array__() | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_l_schema() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in single channel mode | ||||
|     size = (4096, 2 * block_size // 4096) | ||||
|     img = Image.new("L", size, 128) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         img.__arrow_c_schema__() | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_rgba_schema() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         img.__arrow_c_schema__() | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_l_image() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
| 
 | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, 2 * (block_size // 4096)) | ||||
|     img = Image.new("L", size, 128) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     (schema, arr) = img.__arrow_c_array__() | ||||
|     assert schema | ||||
|     assert arr | ||||
| 
 | ||||
|     Image.core.set_use_block_allocator(0) | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_rgba_image() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     (schema, arr) = img.__arrow_c_array__() | ||||
|     assert schema | ||||
|     assert arr | ||||
|     Image.core.set_use_block_allocator(0) | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_l_schema() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in single channel mode | ||||
|     size = (4096, 2 * block_size // 4096) | ||||
|     img = Image.new("L", size, 128) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     schema = img.__arrow_c_schema__() | ||||
|     assert schema | ||||
|     Image.core.set_use_block_allocator(0) | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_rgba_schema() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     schema = img.__arrow_c_schema__() | ||||
|     assert schema | ||||
|     Image.core.set_use_block_allocator(0) | ||||
							
								
								
									
										778
									
								
								Tests/test_file_avif.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										778
									
								
								Tests/test_file_avif.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,778 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import gc | ||||
| import os | ||||
| import re | ||||
| import warnings | ||||
| from collections.abc import Generator, Sequence | ||||
| from contextlib import contextmanager | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import ( | ||||
|     AvifImagePlugin, | ||||
|     Image, | ||||
|     ImageDraw, | ||||
|     ImageFile, | ||||
|     UnidentifiedImageError, | ||||
|     features, | ||||
| ) | ||||
| 
 | ||||
| from .helper import ( | ||||
|     PillowLeakTestCase, | ||||
|     assert_image, | ||||
|     assert_image_similar, | ||||
|     assert_image_similar_tofile, | ||||
|     hopper, | ||||
|     skip_unless_feature, | ||||
| ) | ||||
| 
 | ||||
| try: | ||||
|     from PIL import _avif | ||||
| 
 | ||||
|     HAVE_AVIF = True | ||||
| except ImportError: | ||||
|     HAVE_AVIF = False | ||||
| 
 | ||||
| 
 | ||||
| TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" | ||||
| 
 | ||||
| 
 | ||||
| def assert_xmp_orientation(xmp: bytes, expected: int) -> None: | ||||
|     assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected | ||||
| 
 | ||||
| 
 | ||||
| def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: | ||||
|     out = BytesIO() | ||||
|     im.save(out, "AVIF", **options) | ||||
|     return Image.open(out) | ||||
| 
 | ||||
| 
 | ||||
| def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator: | ||||
|     reason = f"{codec_name} decode not available" | ||||
|     return pytest.mark.skipif( | ||||
|         not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: | ||||
|     reason = f"{codec_name} encode not available" | ||||
|     return pytest.mark.skipif( | ||||
|         not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def is_docker_qemu() -> bool: | ||||
|     try: | ||||
|         init_proc_exe = os.readlink("/proc/1/exe") | ||||
|     except (FileNotFoundError, PermissionError): | ||||
|         return False | ||||
|     return "qemu" in init_proc_exe | ||||
| 
 | ||||
| 
 | ||||
| class TestUnsupportedAvif: | ||||
|     def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) | ||||
| 
 | ||||
|         with pytest.warns(UserWarning): | ||||
|             with pytest.raises(UnidentifiedImageError): | ||||
|                 with Image.open(TEST_AVIF_FILE): | ||||
|                     pass | ||||
| 
 | ||||
|     def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) | ||||
| 
 | ||||
|         with pytest.raises(SyntaxError): | ||||
|             AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("avif") | ||||
| class TestFileAvif: | ||||
|     def test_version(self) -> None: | ||||
|         version = features.version_module("avif") | ||||
|         assert version is not None | ||||
|         assert re.search(r"^\d+\.\d+\.\d+$", version) | ||||
| 
 | ||||
|     def test_codec_version(self) -> None: | ||||
|         assert AvifImagePlugin.get_codec_version("unknown") is None | ||||
| 
 | ||||
|         for codec_name in ("aom", "dav1d", "rav1e", "svt"): | ||||
|             codec_version = AvifImagePlugin.get_codec_version(codec_name) | ||||
|             if _avif.decoder_codec_available( | ||||
|                 codec_name | ||||
|             ) or _avif.encoder_codec_available(codec_name): | ||||
|                 assert codec_version is not None | ||||
|                 assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version) | ||||
|             else: | ||||
|                 assert codec_version is None | ||||
| 
 | ||||
|     def test_read(self) -> None: | ||||
|         """ | ||||
|         Can we read an AVIF file without error? | ||||
|         Does it have the bits we expect? | ||||
|         """ | ||||
| 
 | ||||
|         with Image.open(TEST_AVIF_FILE) as image: | ||||
|             assert image.mode == "RGB" | ||||
|             assert image.size == (128, 128) | ||||
|             assert image.format == "AVIF" | ||||
|             assert image.get_format_mimetype() == "image/avif" | ||||
|             image.getdata() | ||||
| 
 | ||||
|             # generated with: | ||||
|             # avifdec hopper.avif hopper_avif_write.png | ||||
|             assert_image_similar_tofile( | ||||
|                 image, "Tests/images/avif/hopper_avif_write.png", 11.5 | ||||
|             ) | ||||
| 
 | ||||
|     def test_write_rgb(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Can we write a RGB mode file to avif without error? | ||||
|         Does it have the bits we expect? | ||||
|         """ | ||||
| 
 | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
| 
 | ||||
|         im = hopper() | ||||
|         im.save(temp_file) | ||||
|         with Image.open(temp_file) as reloaded: | ||||
|             assert reloaded.mode == "RGB" | ||||
|             assert reloaded.size == (128, 128) | ||||
|             assert reloaded.format == "AVIF" | ||||
|             reloaded.getdata() | ||||
| 
 | ||||
|             # avifdec hopper.avif avif/hopper_avif_write.png | ||||
|             assert_image_similar_tofile( | ||||
|                 reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 | ||||
|             ) | ||||
| 
 | ||||
|             # This test asserts that the images are similar. If the average pixel | ||||
|             # difference between the two images is less than the epsilon value, | ||||
|             # then we're going to accept that it's a reasonable lossy version of | ||||
|             # the image. | ||||
|             assert_image_similar(reloaded, im, 8.62) | ||||
| 
 | ||||
|     def test_AvifEncoder_with_invalid_args(self) -> None: | ||||
|         """ | ||||
|         Calling encoder functions with no arguments should result in an error. | ||||
|         """ | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.AvifEncoder() | ||||
| 
 | ||||
|     def test_AvifDecoder_with_invalid_args(self) -> None: | ||||
|         """ | ||||
|         Calling decoder functions with no arguments should result in an error. | ||||
|         """ | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.AvifDecoder() | ||||
| 
 | ||||
|     def test_invalid_dimensions(self, tmp_path: Path) -> None: | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         im = Image.new("RGB", (0, 0)) | ||||
|         with pytest.raises(ValueError): | ||||
|             im.save(test_file) | ||||
| 
 | ||||
|     def test_encoder_finish_none_error( | ||||
|         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path | ||||
|     ) -> None: | ||||
|         """Save should raise an OSError if AvifEncoder.finish returns None""" | ||||
| 
 | ||||
|         class _mock_avif: | ||||
|             class AvifEncoder: | ||||
|                 def __init__(self, *args: Any) -> None: | ||||
|                     pass | ||||
| 
 | ||||
|                 def add(self, *args: Any) -> None: | ||||
|                     pass | ||||
| 
 | ||||
|                 def finish(self) -> None: | ||||
|                     return None | ||||
| 
 | ||||
|         monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) | ||||
| 
 | ||||
|         im = Image.new("RGB", (150, 150)) | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         with pytest.raises(OSError): | ||||
|             im.save(test_file) | ||||
| 
 | ||||
|     def test_no_resource_warning(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             with warnings.catch_warnings(): | ||||
|                 warnings.simplefilter("error") | ||||
| 
 | ||||
|                 im.save(tmp_path / "temp.avif") | ||||
| 
 | ||||
|     @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) | ||||
|     def test_accept_ftyp_brands(self, major_brand: bytes) -> None: | ||||
|         data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand | ||||
|         assert AvifImagePlugin._accept(data) is True | ||||
| 
 | ||||
|     def test_file_pointer_could_be_reused(self) -> None: | ||||
|         with open(TEST_AVIF_FILE, "rb") as blob: | ||||
|             with Image.open(blob) as im: | ||||
|                 im.load() | ||||
|             with Image.open(blob) as im: | ||||
|                 im.load() | ||||
| 
 | ||||
|     def test_background_from_gif(self, tmp_path: Path) -> None: | ||||
|         with Image.open("Tests/images/chi.gif") as im: | ||||
|             original_value = im.convert("RGB").getpixel((1, 1)) | ||||
| 
 | ||||
|             # Save as AVIF | ||||
|             out_avif = tmp_path / "temp.avif" | ||||
|             im.save(out_avif, save_all=True) | ||||
| 
 | ||||
|         # Save as GIF | ||||
|         out_gif = tmp_path / "temp.gif" | ||||
|         with Image.open(out_avif) as im: | ||||
|             im.save(out_gif) | ||||
| 
 | ||||
|         with Image.open(out_gif) as reread: | ||||
|             reread_value = reread.convert("RGB").getpixel((1, 1)) | ||||
|         difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) | ||||
|         assert difference <= 3 | ||||
| 
 | ||||
|     def test_save_single_frame(self, tmp_path: Path) -> None: | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         with Image.open("Tests/images/chi.gif") as im: | ||||
|             im.save(temp_file) | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert im.n_frames == 1 | ||||
| 
 | ||||
|     def test_invalid_file(self) -> None: | ||||
|         invalid_file = "Tests/images/flower.jpg" | ||||
| 
 | ||||
|         with pytest.raises(SyntaxError): | ||||
|             AvifImagePlugin.AvifImageFile(invalid_file) | ||||
| 
 | ||||
|     def test_load_transparent_rgb(self) -> None: | ||||
|         test_file = "Tests/images/avif/transparency.avif" | ||||
|         with Image.open(test_file) as im: | ||||
|             assert_image(im, "RGBA", (64, 64)) | ||||
| 
 | ||||
|             # image has 876 transparent pixels | ||||
|             assert im.getchannel("A").getcolors()[0] == (876, 0) | ||||
| 
 | ||||
|     def test_save_transparent(self, tmp_path: Path) -> None: | ||||
|         im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) | ||||
|         assert im.getcolors() == [(100, (0, 0, 0, 0))] | ||||
| 
 | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         im.save(test_file) | ||||
| 
 | ||||
|         # check if saved image contains the same transparency | ||||
|         with Image.open(test_file) as im: | ||||
|             assert_image(im, "RGBA", (10, 10)) | ||||
|             assert im.getcolors() == [(100, (0, 0, 0, 0))] | ||||
| 
 | ||||
|     def test_save_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile_none.avif") as im: | ||||
|             assert "icc_profile" not in im.info | ||||
| 
 | ||||
|             with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: | ||||
|                 expected_icc = with_icc.info["icc_profile"] | ||||
|                 assert expected_icc is not None | ||||
| 
 | ||||
|                 im = roundtrip(im, icc_profile=expected_icc) | ||||
|                 assert im.info["icc_profile"] == expected_icc | ||||
| 
 | ||||
|     def test_discard_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile.avif") as im: | ||||
|             im = roundtrip(im, icc_profile=None) | ||||
|         assert "icc_profile" not in im.info | ||||
| 
 | ||||
|     def test_roundtrip_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile.avif") as im: | ||||
|             expected_icc = im.info["icc_profile"] | ||||
| 
 | ||||
|             im = roundtrip(im) | ||||
|         assert im.info["icc_profile"] == expected_icc | ||||
| 
 | ||||
|     def test_roundtrip_no_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile_none.avif") as im: | ||||
|             assert "icc_profile" not in im.info | ||||
| 
 | ||||
|             im = roundtrip(im) | ||||
|         assert "icc_profile" not in im.info | ||||
| 
 | ||||
|     def test_exif(self) -> None: | ||||
|         # With an EXIF chunk | ||||
|         with Image.open("Tests/images/avif/exif.avif") as im: | ||||
|             exif = im.getexif() | ||||
|         assert exif[274] == 1 | ||||
| 
 | ||||
|         with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: | ||||
|             exif = im.getexif() | ||||
|         assert exif[274] == 3 | ||||
| 
 | ||||
|     @pytest.mark.parametrize("use_bytes", [True, False]) | ||||
|     @pytest.mark.parametrize("orientation", [1, 2]) | ||||
|     def test_exif_save( | ||||
|         self, | ||||
|         tmp_path: Path, | ||||
|         use_bytes: bool, | ||||
|         orientation: int, | ||||
|     ) -> None: | ||||
|         exif = Image.Exif() | ||||
|         exif[274] = orientation | ||||
|         exif_data = exif.tobytes() | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, exif=exif_data if use_bytes else exif) | ||||
| 
 | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             if orientation == 1: | ||||
|                 assert "exif" not in reloaded.info | ||||
|             else: | ||||
|                 assert reloaded.info["exif"] == exif_data | ||||
| 
 | ||||
|     def test_exif_without_orientation(self, tmp_path: Path) -> None: | ||||
|         exif = Image.Exif() | ||||
|         exif[272] = b"test" | ||||
|         exif_data = exif.tobytes() | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, exif=exif) | ||||
| 
 | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             assert reloaded.info["exif"] == exif_data | ||||
| 
 | ||||
|     def test_exif_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(SyntaxError): | ||||
|                 im.save(test_file, exif=b"invalid") | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "rot, mir, exif_orientation", | ||||
|         [ | ||||
|             (0, 0, 4), | ||||
|             (0, 1, 2), | ||||
|             (1, 0, 5), | ||||
|             (1, 1, 7), | ||||
|             (2, 0, 2), | ||||
|             (2, 1, 4), | ||||
|             (3, 0, 7), | ||||
|             (3, 1, 5), | ||||
|         ], | ||||
|     ) | ||||
|     def test_rot_mir_exif( | ||||
|         self, rot: int, mir: int, exif_orientation: int, tmp_path: Path | ||||
|     ) -> None: | ||||
|         with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im: | ||||
|             exif = im.getexif() | ||||
|             assert exif[274] == exif_orientation | ||||
| 
 | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, exif=exif) | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             assert reloaded.getexif()[274] == exif_orientation | ||||
| 
 | ||||
|     def test_xmp(self) -> None: | ||||
|         with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: | ||||
|             xmp = im.info["xmp"] | ||||
|         assert_xmp_orientation(xmp, 3) | ||||
| 
 | ||||
|     def test_xmp_save(self, tmp_path: Path) -> None: | ||||
|         xmp_arg = "\n".join( | ||||
|             [ | ||||
|                 '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>', | ||||
|                 '<x:xmpmeta xmlns:x="adobe:ns:meta/">', | ||||
|                 ' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">', | ||||
|                 '  <rdf:Description rdf:about=""', | ||||
|                 '    xmlns:tiff="http://ns.adobe.com/tiff/1.0/"', | ||||
|                 '   tiff:Orientation="1"/>', | ||||
|                 " </rdf:RDF>", | ||||
|                 "</x:xmpmeta>", | ||||
|                 '<?xpacket end="r"?>', | ||||
|             ] | ||||
|         ) | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, xmp=xmp_arg) | ||||
| 
 | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             xmp = reloaded.info["xmp"] | ||||
|         assert_xmp_orientation(xmp, 1) | ||||
| 
 | ||||
|     def test_tell(self) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             assert im.tell() == 0 | ||||
| 
 | ||||
|     def test_seek(self) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             im.seek(0) | ||||
| 
 | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(1) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"]) | ||||
|     def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, subsampling=subsampling) | ||||
| 
 | ||||
|     def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, subsampling="foo") | ||||
| 
 | ||||
|     @pytest.mark.parametrize("value", ["full", "limited"]) | ||||
|     def test_encoder_range(self, tmp_path: Path, value: str) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, range=value) | ||||
| 
 | ||||
|     def test_encoder_range_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, range="foo") | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     def test_encoder_codec_param(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, codec="aom") | ||||
| 
 | ||||
|     def test_encoder_codec_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, codec="foo") | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("dav1d") | ||||
|     def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, codec="dav1d") | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     @pytest.mark.parametrize( | ||||
|         "advanced", | ||||
|         [ | ||||
|             { | ||||
|                 "aq-mode": "1", | ||||
|                 "enable-chroma-deltaq": "1", | ||||
|             }, | ||||
|             (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), | ||||
|             [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], | ||||
|         ], | ||||
|     ) | ||||
|     def test_encoder_advanced_codec_options( | ||||
|         self, advanced: dict[str, str] | Sequence[tuple[str, str]] | ||||
|     ) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             ctrl_buf = BytesIO() | ||||
|             im.save(ctrl_buf, "AVIF", codec="aom") | ||||
|             test_buf = BytesIO() | ||||
|             im.save( | ||||
|                 test_buf, | ||||
|                 "AVIF", | ||||
|                 codec="aom", | ||||
|                 advanced=advanced, | ||||
|             ) | ||||
|             assert ctrl_buf.getvalue() != test_buf.getvalue() | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     @pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234]) | ||||
|     def test_encoder_advanced_codec_options_invalid( | ||||
|         self, tmp_path: Path, advanced: dict[str, str] | int | ||||
|     ) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, codec="aom", advanced=advanced) | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("aom") | ||||
|     def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom") | ||||
| 
 | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             assert im.size == (128, 128) | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("rav1e") | ||||
|     def test_encoder_codec_cannot_decode( | ||||
|         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path | ||||
|     ) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             with Image.open(TEST_AVIF_FILE): | ||||
|                 pass | ||||
| 
 | ||||
|     def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo") | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             with Image.open(TEST_AVIF_FILE): | ||||
|                 pass | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     def test_encoder_codec_available(self) -> None: | ||||
|         assert _avif.encoder_codec_available("aom") is True | ||||
| 
 | ||||
|     def test_encoder_codec_available_bad_params(self) -> None: | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.encoder_codec_available() | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("dav1d") | ||||
|     def test_encoder_codec_available_cannot_decode(self) -> None: | ||||
|         assert _avif.encoder_codec_available("dav1d") is False | ||||
| 
 | ||||
|     def test_encoder_codec_available_invalid(self) -> None: | ||||
|         assert _avif.encoder_codec_available("foo") is False | ||||
| 
 | ||||
|     def test_encoder_quality_valueerror(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, quality="invalid") | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("aom") | ||||
|     def test_decoder_codec_available(self) -> None: | ||||
|         assert _avif.decoder_codec_available("aom") is True | ||||
| 
 | ||||
|     def test_decoder_codec_available_bad_params(self) -> None: | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.decoder_codec_available() | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("rav1e") | ||||
|     def test_decoder_codec_available_cannot_decode(self) -> None: | ||||
|         assert _avif.decoder_codec_available("rav1e") is False | ||||
| 
 | ||||
|     def test_decoder_codec_available_invalid(self) -> None: | ||||
|         assert _avif.decoder_codec_available("foo") is False | ||||
| 
 | ||||
|     def test_p_mode_transparency(self, tmp_path: Path) -> None: | ||||
|         im = Image.new("P", size=(64, 64)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) | ||||
|         draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) | ||||
| 
 | ||||
|         out_png = tmp_path / "temp.png" | ||||
|         im.save(out_png, transparency=0) | ||||
|         with Image.open(out_png) as im_png: | ||||
|             out_avif = tmp_path / "temp.avif" | ||||
|             im_png.save(out_avif, quality=100) | ||||
| 
 | ||||
|             with Image.open(out_avif) as expected: | ||||
|                 assert_image_similar(im_png.convert("RGBA"), expected, 0.17) | ||||
| 
 | ||||
|     def test_decoder_strict_flags(self) -> None: | ||||
|         # This would fail if full avif strictFlags were enabled | ||||
|         with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im: | ||||
|             assert im.size == (128, 128) | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     @pytest.mark.parametrize("speed", [-1, 1, 11]) | ||||
|     def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None: | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         hopper().save(test_file, codec="aom", speed=speed) | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("svt") | ||||
|     def test_svt_optimizations(self, tmp_path: Path) -> None: | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         hopper().save(test_file, codec="svt", speed=1) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("avif") | ||||
| class TestAvifAnimation: | ||||
|     @contextmanager | ||||
|     def star_frames(self) -> Generator[list[Image.Image], None, None]: | ||||
|         with Image.open("Tests/images/avif/star.png") as f: | ||||
|             yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] | ||||
| 
 | ||||
|     def test_n_frames(self) -> None: | ||||
|         """ | ||||
|         Ensure that AVIF format sets n_frames and is_animated attributes | ||||
|         correctly. | ||||
|         """ | ||||
| 
 | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             assert im.n_frames == 1 | ||||
|             assert not im.is_animated | ||||
| 
 | ||||
|         with Image.open("Tests/images/avif/star.avifs") as im: | ||||
|             assert im.n_frames == 5 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
|     def test_write_animation_P(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Convert an animated GIF to animated AVIF, then compare the frame | ||||
|         count, and ensure the frames are visually similar to the originals. | ||||
|         """ | ||||
| 
 | ||||
|         with Image.open("Tests/images/avif/star.gif") as original: | ||||
|             assert original.n_frames > 1 | ||||
| 
 | ||||
|             temp_file = tmp_path / "temp.avif" | ||||
|             original.save(temp_file, save_all=True) | ||||
|             with Image.open(temp_file) as im: | ||||
|                 assert im.n_frames == original.n_frames | ||||
| 
 | ||||
|                 # Compare first frame in P mode to frame from original GIF | ||||
|                 assert_image_similar(im, original.convert("RGBA"), 2) | ||||
| 
 | ||||
|                 # Compare later frames in RGBA mode to frames from original GIF | ||||
|                 for frame in range(1, original.n_frames): | ||||
|                     original.seek(frame) | ||||
|                     im.seek(frame) | ||||
|                     assert_image_similar(im, original, 2.54) | ||||
| 
 | ||||
|     def test_write_animation_RGBA(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Write an animated AVIF from RGBA frames, and ensure the frames | ||||
|         are visually similar to the originals. | ||||
|         """ | ||||
| 
 | ||||
|         def check(temp_file: Path) -> None: | ||||
|             with Image.open(temp_file) as im: | ||||
|                 assert im.n_frames == 4 | ||||
| 
 | ||||
|                 # Compare first frame to original | ||||
|                 assert_image_similar(im, frame1, 2.7) | ||||
| 
 | ||||
|                 # Compare second frame to original | ||||
|                 im.seek(1) | ||||
|                 assert_image_similar(im, frame2, 4.1) | ||||
| 
 | ||||
|         with self.star_frames() as frames: | ||||
|             frame1 = frames[0] | ||||
|             frame2 = frames[1] | ||||
|             temp_file1 = tmp_path / "temp.avif" | ||||
|             frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) | ||||
|             check(temp_file1) | ||||
| 
 | ||||
|             # Test appending using a generator | ||||
|             def imGenerator( | ||||
|                 ims: list[Image.Image], | ||||
|             ) -> Generator[Image.Image, None, None]: | ||||
|                 yield from ims | ||||
| 
 | ||||
|             temp_file2 = tmp_path / "temp_generator.avif" | ||||
|             frames[0].copy().save( | ||||
|                 temp_file2, | ||||
|                 save_all=True, | ||||
|                 append_images=imGenerator(frames[1:]), | ||||
|             ) | ||||
|             check(temp_file2) | ||||
| 
 | ||||
|     def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         frame1 = Image.new("RGB", (100, 100)) | ||||
|         frame2 = Image.new("RGB", (150, 150)) | ||||
|         with pytest.raises(ValueError): | ||||
|             frame1.save(temp_file, save_all=True, append_images=[frame2]) | ||||
| 
 | ||||
|     def test_heif_raises_unidentified_image_error(self) -> None: | ||||
|         with pytest.raises(UnidentifiedImageError): | ||||
|             with Image.open("Tests/images/avif/hopper.heif"): | ||||
|                 pass | ||||
| 
 | ||||
|     @pytest.mark.parametrize("alpha_premultiplied", [False, True]) | ||||
|     def test_alpha_premultiplied( | ||||
|         self, tmp_path: Path, alpha_premultiplied: bool | ||||
|     ) -> None: | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         color = (200, 200, 200, 1) | ||||
|         im = Image.new("RGBA", (1, 1), color) | ||||
|         im.save(temp_file, alpha_premultiplied=alpha_premultiplied) | ||||
| 
 | ||||
|         expected = (255, 255, 255, 1) if alpha_premultiplied else color | ||||
|         with Image.open(temp_file) as reloaded: | ||||
|             assert reloaded.getpixel((0, 0)) == expected | ||||
| 
 | ||||
|     def test_timestamp_and_duration(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Try passing a list of durations, and make sure the encoded | ||||
|         timestamps and durations are correct. | ||||
|         """ | ||||
| 
 | ||||
|         durations = [1, 10, 20, 30, 40] | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         with self.star_frames() as frames: | ||||
|             frames[0].save( | ||||
|                 temp_file, | ||||
|                 save_all=True, | ||||
|                 append_images=(frames[1:] + [frames[0]]), | ||||
|                 duration=durations, | ||||
|             ) | ||||
| 
 | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert im.n_frames == 5 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
|             # Check that timestamps and durations match original values specified | ||||
|             timestamp = 0 | ||||
|             for frame in range(im.n_frames): | ||||
|                 im.seek(frame) | ||||
|                 im.load() | ||||
|                 assert im.info["duration"] == durations[frame] | ||||
|                 assert im.info["timestamp"] == timestamp | ||||
|                 timestamp += durations[frame] | ||||
| 
 | ||||
|     def test_seeking(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Create an animated AVIF file, and then try seeking through frames in | ||||
|         reverse-order, verifying the timestamps and durations are correct. | ||||
|         """ | ||||
| 
 | ||||
|         duration = 33 | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         with self.star_frames() as frames: | ||||
|             frames[0].save( | ||||
|                 temp_file, | ||||
|                 save_all=True, | ||||
|                 append_images=(frames[1:] + [frames[0]]), | ||||
|                 duration=duration, | ||||
|             ) | ||||
| 
 | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert im.n_frames == 5 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
|             # Traverse frames in reverse, checking timestamps and durations | ||||
|             timestamp = duration * (im.n_frames - 1) | ||||
|             for frame in reversed(range(im.n_frames)): | ||||
|                 im.seek(frame) | ||||
|                 im.load() | ||||
|                 assert im.info["duration"] == duration | ||||
|                 assert im.info["timestamp"] == timestamp | ||||
|                 timestamp -= duration | ||||
| 
 | ||||
|     def test_seek_errors(self) -> None: | ||||
|         with Image.open("Tests/images/avif/star.avifs") as im: | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(-1) | ||||
| 
 | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(42) | ||||
| 
 | ||||
| 
 | ||||
| MAX_THREADS = os.cpu_count() or 1 | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("avif") | ||||
| class TestAvifLeaks(PillowLeakTestCase): | ||||
|     mem_limit = MAX_THREADS * 3 * 1024 | ||||
|     iterations = 100 | ||||
| 
 | ||||
|     @pytest.mark.skipif( | ||||
|         is_docker_qemu(), reason="Skipping on cross-architecture containers" | ||||
|     ) | ||||
|     def test_leak_load(self) -> None: | ||||
|         with open(TEST_AVIF_FILE, "rb") as f: | ||||
|             im_data = f.read() | ||||
| 
 | ||||
|         def core() -> None: | ||||
|             with Image.open(BytesIO(im_data)) as im: | ||||
|                 im.load() | ||||
|             gc.collect() | ||||
| 
 | ||||
|         self._test_leak(core) | ||||
|  | @ -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)) | ||||
|  | @ -258,6 +258,15 @@ class TestImage: | |||
|             assert im.readonly | ||||
|             im.save(temp_file) | ||||
| 
 | ||||
|     def test_save_without_changing_readonly(self, tmp_path: Path) -> None: | ||||
|         temp_file = tmp_path / "temp.bmp" | ||||
| 
 | ||||
|         with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: | ||||
|             assert im.readonly | ||||
| 
 | ||||
|             im.save(temp_file) | ||||
|             assert im.readonly | ||||
| 
 | ||||
|     def test_dump(self, tmp_path: Path) -> None: | ||||
|         im = Image.new("L", (10, 10)) | ||||
|         im._dump(str(tmp_path / "temp_L.ppm")) | ||||
|  |  | |||
|  | @ -40,8 +40,10 @@ class TestImageGrab: | |||
| 
 | ||||
|     @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") | ||||
|     def test_grab_no_xcb(self) -> None: | ||||
|         if sys.platform not in ("win32", "darwin") and not shutil.which( | ||||
|             "gnome-screenshot" | ||||
|         if ( | ||||
|             sys.platform not in ("win32", "darwin") | ||||
|             and not shutil.which("gnome-screenshot") | ||||
|             and not shutil.which("spectacle") | ||||
|         ): | ||||
|             with pytest.raises(OSError) as e: | ||||
|                 ImageGrab.grab() | ||||
|  | @ -57,6 +59,13 @@ class TestImageGrab: | |||
|             ImageGrab.grab(xdisplay="error.test:0.0") | ||||
|         assert str(e.value).startswith("X connection failed") | ||||
| 
 | ||||
|     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") | ||||
|     def test_grab_invalid_handle(self) -> None: | ||||
|         with pytest.raises(OSError, match="unable to get device context for handle"): | ||||
|             ImageGrab.grab(window=-1) | ||||
|         with pytest.raises(OSError, match="screen grab failed"): | ||||
|             ImageGrab.grab(window=0) | ||||
| 
 | ||||
|     def test_grabclipboard(self) -> None: | ||||
|         if sys.platform == "darwin": | ||||
|             subprocess.call(["screencapture", "-cx"]) | ||||
|  |  | |||
|  | @ -83,6 +83,7 @@ def test_pickle_jpeg() -> None: | |||
|         unpickled_image = pickle.loads(pickle.dumps(image)) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert unpickled_image.filename == "Tests/images/hopper.jpg" | ||||
|     assert len(unpickled_image.layer) == 3 | ||||
|     assert unpickled_image.layers == 3 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										112
									
								
								Tests/test_pyarrow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Tests/test_pyarrow.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import Any  # undone | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_deep_equal, | ||||
|     assert_image_equal, | ||||
|     hopper, | ||||
| ) | ||||
| 
 | ||||
| pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") | ||||
| 
 | ||||
| TEST_IMAGE_SIZE = (10, 10) | ||||
| 
 | ||||
| 
 | ||||
| def _test_img_equals_pyarray( | ||||
|     img: Image.Image, arr: Any, mask: list[int] | None | ||||
| ) -> None: | ||||
|     assert img.height * img.width == len(arr) | ||||
|     px = img.load() | ||||
|     assert px is not None | ||||
|     for x in range(0, img.size[0], int(img.size[0] / 10)): | ||||
|         for y in range(0, img.size[1], int(img.size[1] / 10)): | ||||
|             if mask: | ||||
|                 for ix, elt in enumerate(mask): | ||||
|                     pixel = px[x, y] | ||||
|                     assert isinstance(pixel, tuple) | ||||
|                     assert pixel[ix] == arr[y * img.width + x].as_py()[elt] | ||||
|             else: | ||||
|                 assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) | ||||
| 
 | ||||
| 
 | ||||
| # really hard to get a non-nullable list type | ||||
| fl_uint8_4_type = pyarrow.field( | ||||
|     "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) | ||||
| ).type | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, dtype, mask", | ||||
|     ( | ||||
|         ("L", pyarrow.uint8(), None), | ||||
|         ("I", pyarrow.int32(), None), | ||||
|         ("F", pyarrow.float32(), None), | ||||
|         ("LA", fl_uint8_4_type, [0, 3]), | ||||
|         ("RGB", fl_uint8_4_type, [0, 1, 2]), | ||||
|         ("RGBA", fl_uint8_4_type, None), | ||||
|         ("RGBX", fl_uint8_4_type, None), | ||||
|         ("CMYK", fl_uint8_4_type, None), | ||||
|         ("YCbCr", fl_uint8_4_type, [0, 1, 2]), | ||||
|         ("HSV", fl_uint8_4_type, [0, 1, 2]), | ||||
|     ), | ||||
| ) | ||||
| def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: | ||||
|     img = hopper(mode) | ||||
| 
 | ||||
|     # Resize to non-square | ||||
|     img = img.crop((3, 0, 124, 127)) | ||||
|     assert img.size == (121, 127) | ||||
| 
 | ||||
|     arr = pyarrow.array(img) | ||||
|     _test_img_equals_pyarray(img, arr, mask) | ||||
|     assert arr.type == dtype | ||||
| 
 | ||||
|     reloaded = Image.fromarrow(arr, mode, img.size) | ||||
| 
 | ||||
|     assert reloaded | ||||
| 
 | ||||
|     assert_image_equal(img, reloaded) | ||||
| 
 | ||||
| 
 | ||||
| def test_lifetime() -> None: | ||||
|     # valgrind shouldn't error out here. | ||||
|     # arrays should be accessible after the image is deleted. | ||||
| 
 | ||||
|     img = hopper("L") | ||||
| 
 | ||||
|     arr_1 = pyarrow.array(img) | ||||
|     arr_2 = pyarrow.array(img) | ||||
| 
 | ||||
|     del img | ||||
| 
 | ||||
|     assert arr_1.sum().as_py() > 0 | ||||
|     del arr_1 | ||||
| 
 | ||||
|     assert arr_2.sum().as_py() > 0 | ||||
|     del arr_2 | ||||
| 
 | ||||
| 
 | ||||
| def test_lifetime2() -> None: | ||||
|     # valgrind shouldn't error out here. | ||||
|     # img should remain after the arrays are collected. | ||||
| 
 | ||||
|     img = hopper("L") | ||||
| 
 | ||||
|     arr_1 = pyarrow.array(img) | ||||
|     arr_2 = pyarrow.array(img) | ||||
| 
 | ||||
|     assert arr_1.sum().as_py() > 0 | ||||
|     del arr_1 | ||||
| 
 | ||||
|     assert arr_2.sum().as_py() > 0 | ||||
|     del arr_2 | ||||
| 
 | ||||
|     img2 = img.copy() | ||||
|     px = img2.load() | ||||
|     assert px  # make mypy happy | ||||
|     assert isinstance(px[0, 0], int) | ||||
							
								
								
									
										64
									
								
								depends/install_libavif.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										64
									
								
								depends/install_libavif.sh
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| #!/usr/bin/env bash | ||||
| set -eo pipefail | ||||
| 
 | ||||
| version=1.2.1 | ||||
| 
 | ||||
| ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz | ||||
| 
 | ||||
| pushd libavif-$version | ||||
| 
 | ||||
| if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then | ||||
|     PREFIX=$(brew --prefix) | ||||
| else | ||||
|     PREFIX=/usr | ||||
| fi | ||||
| 
 | ||||
| PKGCONFIG=${PKGCONFIG:-pkg-config} | ||||
| 
 | ||||
| LIBAVIF_CMAKE_FLAGS=() | ||||
| HAS_DECODER=0 | ||||
| HAS_ENCODER=0 | ||||
| 
 | ||||
| if $PKGCONFIG --exists aom; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) | ||||
|     HAS_ENCODER=1 | ||||
|     HAS_DECODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists dav1d; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) | ||||
|     HAS_DECODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists libgav1; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) | ||||
|     HAS_DECODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists rav1e; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) | ||||
|     HAS_ENCODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists SvtAv1Enc; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) | ||||
|     HAS_ENCODER=1 | ||||
| fi | ||||
| 
 | ||||
| if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) | ||||
| fi | ||||
| 
 | ||||
| cmake \ | ||||
|     -DCMAKE_INSTALL_PREFIX=$PREFIX \ | ||||
|     -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ | ||||
|     -DCMAKE_BUILD_TYPE=Release \ | ||||
|     -DCMAKE_MACOSX_RPATH=OFF \ | ||||
|     -DAVIF_LIBSHARPYUV=LOCAL \ | ||||
|     -DAVIF_LIBYUV=LOCAL \ | ||||
|     "${LIBAVIF_CMAKE_FLAGS[@]}" \ | ||||
|     . | ||||
| 
 | ||||
| sudo make install | ||||
| 
 | ||||
| popd | ||||
|  | @ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. | |||
| Fully supported formats | ||||
| ----------------------- | ||||
| 
 | ||||
| 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. | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,15 +9,16 @@ or the clipboard to a PIL image memory. | |||
| 
 | ||||
| .. versionadded:: 1.1.3 | ||||
| 
 | ||||
| .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) | ||||
| .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None, window=None) | ||||
| 
 | ||||
|     Take a snapshot of the screen. The pixels inside the bounding box are returned as | ||||
|     an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, | ||||
|     the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. | ||||
| 
 | ||||
|     On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return | ||||
|     a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is | ||||
|     installed. To disable this behaviour, pass ``xdisplay=""`` instead. | ||||
|     a snapshot of the screen, ``gnome-screenshot`` or ``spectacle`` will be used as a | ||||
|     fallback if they are installed. To disable this behaviour, pass ``xdisplay=""`` | ||||
|     instead. | ||||
| 
 | ||||
|     .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) | ||||
| 
 | ||||
|  | @ -39,6 +40,11 @@ or the clipboard to a PIL image memory. | |||
|         You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. | ||||
| 
 | ||||
|         .. versionadded:: 7.1.0 | ||||
| 
 | ||||
|     :param window: | ||||
|         HWND, to capture a single window. Windows only. | ||||
| 
 | ||||
|         .. versionadded:: 11.2.0 | ||||
|     :return: An image | ||||
| 
 | ||||
| .. py:function:: grabclipboard() | ||||
|  |  | |||
							
								
								
									
										88
									
								
								docs/reference/arrow_support.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								docs/reference/arrow_support.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| .. _arrow-support: | ||||
| 
 | ||||
| ============= | ||||
| Arrow Support | ||||
| ============= | ||||
| 
 | ||||
| `Arrow <https://arrow.apache.org/>`__ | ||||
| is an in-memory data exchange format that is the spiritual | ||||
| successor to the NumPy array interface. It provides for zero-copy | ||||
| access to columnar data, which in our case is ``Image`` data. | ||||
| 
 | ||||
| The goal with Arrow is to provide native zero-copy interoperability | ||||
| with any Arrow provider or consumer in the Python ecosystem. | ||||
| 
 | ||||
| .. warning:: Zero-copy does not mean zero allocation -- the internal | ||||
|   memory layout of Pillow images contains an allocation for row | ||||
|   pointers, so there is a non-zero, but significantly smaller than a | ||||
|   full-copy memory cost to reading an Arrow image. | ||||
| 
 | ||||
| 
 | ||||
| Data Formats | ||||
| ============ | ||||
| 
 | ||||
| Pillow currently supports exporting Arrow images in all modes | ||||
| **except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to | ||||
| line-length packing in these modes making for non-continuous memory. | ||||
| 
 | ||||
| For single-band images, the exported array is width*height elements, | ||||
| with each pixel corresponding to the appropriate Arrow type. | ||||
| 
 | ||||
| For multiband images, the exported array is width*height fixed-length | ||||
| four-element arrays of uint8. This is memory compatible with the raw | ||||
| image storage of four bytes per pixel. | ||||
| 
 | ||||
| Mode ``1`` images are exported as one uint8 byte/pixel, as this is | ||||
| consistent with the internal storage. | ||||
| 
 | ||||
| Pillow will accept, but not produce, one other format. For any | ||||
| multichannel image with 32-bit storage per pixel, Pillow will accept | ||||
| an array of width*height int32 elements, which will then be | ||||
| interpreted using the mode-specific interpretation of the bytes. | ||||
| 
 | ||||
| The image mode must match the Arrow band format when reading single | ||||
| channel images. | ||||
| 
 | ||||
| Memory Allocator | ||||
| ================ | ||||
| 
 | ||||
| Pillow's default memory allocator, the :ref:`block_allocator`, | ||||
| allocates up to a 16 MB block for images by default. Larger images | ||||
| overflow into additional blocks. Arrow requires a single continuous | ||||
| memory allocation, so images allocated in multiple blocks cannot be | ||||
| exported in the Arrow format. | ||||
| 
 | ||||
| To enable the single block allocator:: | ||||
| 
 | ||||
|   from PIL import Image | ||||
|   Image.core.set_use_block_allocator(1) | ||||
| 
 | ||||
| Note that this is a global setting, not a per-image setting. | ||||
| 
 | ||||
| Unsupported Features | ||||
| ==================== | ||||
| 
 | ||||
| * Table/dataframe protocol. We support a single array. | ||||
| * Null markers, producing or consuming. Null values are inferred from | ||||
|   the mode, e.g. RGB images are stored in the first three bytes of | ||||
|   each 32-bit pixel, and the last byte is an implied null. | ||||
| * Schema negotiation. There is an optional schema for the requested | ||||
|   datatype in the Arrow source interface. We ignore that | ||||
|   parameter. | ||||
| * Array metadata. | ||||
| 
 | ||||
| Internal Details | ||||
| ================ | ||||
| 
 | ||||
| Python Arrow C interface: | ||||
| https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html | ||||
| 
 | ||||
| The memory that is exported from the Arrow interface is shared -- not | ||||
| copied, so the lifetime of the memory allocation is no longer strictly | ||||
| tied to the life of the Python object. | ||||
| 
 | ||||
| The core imaging struct now has a refcount associated with it, and the | ||||
| lifetime of the core image struct is now divorced from the Python | ||||
| image object. Creating an arrow reference to the image increments the | ||||
| refcount, and the imaging struct is only released when the refcount | ||||
| reaches zero. | ||||
|  | @ -1,3 +1,6 @@ | |||
| 
 | ||||
| .. _block_allocator: | ||||
| 
 | ||||
| Block Allocator | ||||
| =============== | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -9,3 +9,4 @@ Internal Reference | |||
|   block_allocator | ||||
|   internal_modules | ||||
|   c_extension_debugging | ||||
|   arrow_support | ||||
|  |  | |||
|  | @ -1,6 +1,14 @@ | |||
| Plugin reference | ||||
| ================ | ||||
| 
 | ||||
| :mod:`~PIL.AvifImagePlugin` Module | ||||
| ---------------------------------- | ||||
| 
 | ||||
| .. automodule:: PIL.AvifImagePlugin | ||||
|     :members: | ||||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
| 
 | ||||
| :mod:`~PIL.BmpImagePlugin` Module | ||||
| --------------------------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,6 +51,15 @@ aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: | |||
|     draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") | ||||
|     draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") | ||||
| 
 | ||||
| Specify window in ImageGrab on Windows | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the | ||||
| HWND:: | ||||
| 
 | ||||
|     from PIL import ImageGrab | ||||
|     ImageGrab.grab(window=hwnd) | ||||
| 
 | ||||
| Check for MozJPEG | ||||
| ^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
|  | @ -68,3 +77,34 @@ 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 | ||||
| ============= | ||||
| 
 | ||||
| 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. | ||||
| 
 | ||||
| To create 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)) | ||||
| 
 | ||||
| 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) | ||||
| 
 | ||||
| Reading and writing AVIF images | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Pillow can now read and write AVIF images. If you are building Pillow from source, this | ||||
| will require libavif 1.0.0 or later. | ||||
|  |  | |||
|  | @ -54,6 +54,10 @@ optional-dependencies.fpx = [ | |||
| optional-dependencies.mic = [ | ||||
|   "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'", | ||||
| ] | ||||
|  |  | |||
							
								
								
									
										20
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								setup.py
									
									
									
									
									
								
							|  | @ -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
									
								
							
							
						
						
									
										292
									
								
								src/PIL/AvifImagePlugin.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,292 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import os | ||||
| from io import BytesIO | ||||
| from typing import IO | ||||
| 
 | ||||
| from . import ExifTags, Image, ImageFile | ||||
| 
 | ||||
| try: | ||||
|     from . import _avif | ||||
| 
 | ||||
|     SUPPORTED = True | ||||
| except ImportError: | ||||
|     SUPPORTED = False | ||||
| 
 | ||||
| # Decoder options as module globals, until there is a way to pass parameters | ||||
| # to Image.open (see https://github.com/python-pillow/Pillow/issues/569) | ||||
| DECODE_CODEC_CHOICE = "auto" | ||||
| # Decoding is only affected by this for libavif **0.8.4** or greater. | ||||
| DEFAULT_MAX_THREADS = 0 | ||||
| 
 | ||||
| 
 | ||||
| def get_codec_version(codec_name: str) -> str | None: | ||||
|     versions = _avif.codec_versions() | ||||
|     for version in versions.split(", "): | ||||
|         if version.split(" [")[0] == codec_name: | ||||
|             return version.split(":")[-1].split(" ")[0] | ||||
|     return None | ||||
| 
 | ||||
| 
 | ||||
| def _accept(prefix: bytes) -> bool | str: | ||||
|     if prefix[4:8] != b"ftyp": | ||||
|         return False | ||||
|     major_brand = prefix[8:12] | ||||
|     if major_brand in ( | ||||
|         # coding brands | ||||
|         b"avif", | ||||
|         b"avis", | ||||
|         # We accept files with AVIF container brands; we can't yet know if | ||||
|         # the ftyp box has the correct compatible brands, but if it doesn't | ||||
|         # then the plugin will raise a SyntaxError which Pillow will catch | ||||
|         # before moving on to the next plugin that accepts the file. | ||||
|         # | ||||
|         # Also, because this file might not actually be an AVIF file, we | ||||
|         # don't raise an error if AVIF support isn't properly compiled. | ||||
|         b"mif1", | ||||
|         b"msf1", | ||||
|     ): | ||||
|         if not SUPPORTED: | ||||
|             return ( | ||||
|                 "image file could not be identified because AVIF support not installed" | ||||
|             ) | ||||
|         return True | ||||
|     return False | ||||
| 
 | ||||
| 
 | ||||
| def _get_default_max_threads() -> int: | ||||
|     if DEFAULT_MAX_THREADS: | ||||
|         return DEFAULT_MAX_THREADS | ||||
|     if hasattr(os, "sched_getaffinity"): | ||||
|         return len(os.sched_getaffinity(0)) | ||||
|     else: | ||||
|         return os.cpu_count() or 1 | ||||
| 
 | ||||
| 
 | ||||
| class AvifImageFile(ImageFile.ImageFile): | ||||
|     format = "AVIF" | ||||
|     format_description = "AVIF image" | ||||
|     __frame = -1 | ||||
| 
 | ||||
|     def _open(self) -> None: | ||||
|         if not SUPPORTED: | ||||
|             msg = "image file could not be opened because AVIF support not installed" | ||||
|             raise SyntaxError(msg) | ||||
| 
 | ||||
|         if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available( | ||||
|             DECODE_CODEC_CHOICE | ||||
|         ): | ||||
|             msg = "Invalid opening codec" | ||||
|             raise ValueError(msg) | ||||
|         self._decoder = _avif.AvifDecoder( | ||||
|             self.fp.read(), | ||||
|             DECODE_CODEC_CHOICE, | ||||
|             _get_default_max_threads(), | ||||
|         ) | ||||
| 
 | ||||
|         # Get info from decoder | ||||
|         self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = ( | ||||
|             self._decoder.get_info() | ||||
|         ) | ||||
|         self.is_animated = self.n_frames > 1 | ||||
| 
 | ||||
|         if icc: | ||||
|             self.info["icc_profile"] = icc | ||||
|         if xmp: | ||||
|             self.info["xmp"] = xmp | ||||
| 
 | ||||
|         if exif_orientation != 1 or exif: | ||||
|             exif_data = Image.Exif() | ||||
|             if exif: | ||||
|                 exif_data.load(exif) | ||||
|                 original_orientation = exif_data.get(ExifTags.Base.Orientation, 1) | ||||
|             else: | ||||
|                 original_orientation = 1 | ||||
|             if exif_orientation != original_orientation: | ||||
|                 exif_data[ExifTags.Base.Orientation] = exif_orientation | ||||
|                 exif = exif_data.tobytes() | ||||
|         if exif: | ||||
|             self.info["exif"] = exif | ||||
|         self.seek(0) | ||||
| 
 | ||||
|     def seek(self, frame: int) -> None: | ||||
|         if not self._seek_check(frame): | ||||
|             return | ||||
| 
 | ||||
|         # Set tile | ||||
|         self.__frame = frame | ||||
|         self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)] | ||||
| 
 | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if self.tile: | ||||
|             # We need to load the image data for this frame | ||||
|             data, timescale, pts_in_timescales, duration_in_timescales = ( | ||||
|                 self._decoder.get_frame(self.__frame) | ||||
|             ) | ||||
|             self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale)) | ||||
|             self.info["duration"] = round(1000 * (duration_in_timescales / timescale)) | ||||
| 
 | ||||
|             if self.fp and self._exclusive_fp: | ||||
|                 self.fp.close() | ||||
|             self.fp = BytesIO(data) | ||||
| 
 | ||||
|         return super().load() | ||||
| 
 | ||||
|     def load_seek(self, pos: int) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def tell(self) -> int: | ||||
|         return self.__frame | ||||
| 
 | ||||
| 
 | ||||
| def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||
|     _save(im, fp, filename, save_all=True) | ||||
| 
 | ||||
| 
 | ||||
| def _save( | ||||
|     im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False | ||||
| ) -> None: | ||||
|     info = im.encoderinfo.copy() | ||||
|     if save_all: | ||||
|         append_images = list(info.get("append_images", [])) | ||||
|     else: | ||||
|         append_images = [] | ||||
| 
 | ||||
|     total = 0 | ||||
|     for ims in [im] + append_images: | ||||
|         total += getattr(ims, "n_frames", 1) | ||||
| 
 | ||||
|     quality = info.get("quality", 75) | ||||
|     if not isinstance(quality, int) or quality < 0 or quality > 100: | ||||
|         msg = "Invalid quality setting" | ||||
|         raise ValueError(msg) | ||||
| 
 | ||||
|     duration = info.get("duration", 0) | ||||
|     subsampling = info.get("subsampling", "4:2:0") | ||||
|     speed = info.get("speed", 6) | ||||
|     max_threads = info.get("max_threads", _get_default_max_threads()) | ||||
|     codec = info.get("codec", "auto") | ||||
|     if codec != "auto" and not _avif.encoder_codec_available(codec): | ||||
|         msg = "Invalid saving codec" | ||||
|         raise ValueError(msg) | ||||
|     range_ = info.get("range", "full") | ||||
|     tile_rows_log2 = info.get("tile_rows", 0) | ||||
|     tile_cols_log2 = info.get("tile_cols", 0) | ||||
|     alpha_premultiplied = bool(info.get("alpha_premultiplied", False)) | ||||
|     autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0)) | ||||
| 
 | ||||
|     icc_profile = info.get("icc_profile", im.info.get("icc_profile")) | ||||
|     exif_orientation = 1 | ||||
|     if exif := info.get("exif"): | ||||
|         if isinstance(exif, Image.Exif): | ||||
|             exif_data = exif | ||||
|         else: | ||||
|             exif_data = Image.Exif() | ||||
|             exif_data.load(exif) | ||||
|         if ExifTags.Base.Orientation in exif_data: | ||||
|             exif_orientation = exif_data.pop(ExifTags.Base.Orientation) | ||||
|             exif = exif_data.tobytes() if exif_data else b"" | ||||
|         elif isinstance(exif, Image.Exif): | ||||
|             exif = exif_data.tobytes() | ||||
| 
 | ||||
|     xmp = info.get("xmp") | ||||
| 
 | ||||
|     if isinstance(xmp, str): | ||||
|         xmp = xmp.encode("utf-8") | ||||
| 
 | ||||
|     advanced = info.get("advanced") | ||||
|     if advanced is not None: | ||||
|         if isinstance(advanced, dict): | ||||
|             advanced = advanced.items() | ||||
|         try: | ||||
|             advanced = tuple(advanced) | ||||
|         except TypeError: | ||||
|             invalid = True | ||||
|         else: | ||||
|             invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced) | ||||
|         if invalid: | ||||
|             msg = ( | ||||
|                 "advanced codec options must be a dict of key-value string " | ||||
|                 "pairs or a series of key-value two-tuples" | ||||
|             ) | ||||
|             raise ValueError(msg) | ||||
| 
 | ||||
|     # Setup the AVIF encoder | ||||
|     enc = _avif.AvifEncoder( | ||||
|         im.size, | ||||
|         subsampling, | ||||
|         quality, | ||||
|         speed, | ||||
|         max_threads, | ||||
|         codec, | ||||
|         range_, | ||||
|         tile_rows_log2, | ||||
|         tile_cols_log2, | ||||
|         alpha_premultiplied, | ||||
|         autotiling, | ||||
|         icc_profile or b"", | ||||
|         exif or b"", | ||||
|         exif_orientation, | ||||
|         xmp or b"", | ||||
|         advanced, | ||||
|     ) | ||||
| 
 | ||||
|     # Add each frame | ||||
|     frame_idx = 0 | ||||
|     frame_duration = 0 | ||||
|     cur_idx = im.tell() | ||||
|     is_single_frame = total == 1 | ||||
|     try: | ||||
|         for ims in [im] + append_images: | ||||
|             # Get number of frames in this image | ||||
|             nfr = getattr(ims, "n_frames", 1) | ||||
| 
 | ||||
|             for idx in range(nfr): | ||||
|                 ims.seek(idx) | ||||
| 
 | ||||
|                 # Make sure image mode is supported | ||||
|                 frame = ims | ||||
|                 rawmode = ims.mode | ||||
|                 if ims.mode not in {"RGB", "RGBA"}: | ||||
|                     rawmode = "RGBA" if ims.has_transparency_data else "RGB" | ||||
|                     frame = ims.convert(rawmode) | ||||
| 
 | ||||
|                 # Update frame duration | ||||
|                 if isinstance(duration, (list, tuple)): | ||||
|                     frame_duration = duration[frame_idx] | ||||
|                 else: | ||||
|                     frame_duration = duration | ||||
| 
 | ||||
|                 # Append the frame to the animation encoder | ||||
|                 enc.add( | ||||
|                     frame.tobytes("raw", rawmode), | ||||
|                     frame_duration, | ||||
|                     frame.size, | ||||
|                     rawmode, | ||||
|                     is_single_frame, | ||||
|                 ) | ||||
| 
 | ||||
|                 # Update frame index | ||||
|                 frame_idx += 1 | ||||
| 
 | ||||
|                 if not save_all: | ||||
|                     break | ||||
| 
 | ||||
|     finally: | ||||
|         im.seek(cur_idx) | ||||
| 
 | ||||
|     # Get the final output from the encoder | ||||
|     data = enc.finish() | ||||
|     if data is None: | ||||
|         msg = "cannot write file as AVIF (encoder returned None)" | ||||
|         raise OSError(msg) | ||||
| 
 | ||||
|     fp.write(data) | ||||
| 
 | ||||
| 
 | ||||
| Image.register_open(AvifImageFile.format, AvifImageFile, _accept) | ||||
| if SUPPORTED: | ||||
|     Image.register_save(AvifImageFile.format, _save) | ||||
|     Image.register_save_all(AvifImageFile.format, _save_all) | ||||
|     Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"]) | ||||
|     Image.register_mime(AvifImageFile.format, "image/avif") | ||||
|  | @ -577,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 | ||||
|  | @ -723,6 +731,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] | ||||
|  | @ -1519,6 +1537,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: | ||||
|  | @ -2519,8 +2539,15 @@ class Image: | |||
|                 msg = f"unknown file extension: {ext}" | ||||
|                 raise ValueError(msg) from e | ||||
| 
 | ||||
|         from . import ImageFile | ||||
| 
 | ||||
|         # may mutate self! | ||||
|         self._ensure_mutable() | ||||
|         if isinstance(self, ImageFile.ImageFile) and os.path.abspath( | ||||
|             filename | ||||
|         ) == os.path.abspath(self.filename): | ||||
|             self._ensure_mutable() | ||||
|         else: | ||||
|             self.load() | ||||
| 
 | ||||
|         save_all = params.pop("save_all", None) | ||||
|         self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} | ||||
|  | @ -3198,6 +3225,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 | ||||
|  | @ -3286,6 +3325,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 | ||||
|  |  | |||
|  | @ -263,8 +263,12 @@ class ImageFile(Image.Image): | |||
|             return Image.MIME.get(self.format.upper()) | ||||
|         return None | ||||
| 
 | ||||
|     def __getstate__(self) -> list[Any]: | ||||
|         return super().__getstate__() + [self.filename] | ||||
| 
 | ||||
|     def __setstate__(self, state: list[Any]) -> None: | ||||
|         self.tile = [] | ||||
|         self.filename = state[5] | ||||
|         super().__setstate__(state) | ||||
| 
 | ||||
|     def verify(self) -> None: | ||||
|  |  | |||
|  | @ -25,12 +25,17 @@ import tempfile | |||
| 
 | ||||
| from . import Image | ||||
| 
 | ||||
| TYPE_CHECKING = False | ||||
| if TYPE_CHECKING: | ||||
|     from . import ImageWin | ||||
| 
 | ||||
| 
 | ||||
| def grab( | ||||
|     bbox: tuple[int, int, int, int] | None = None, | ||||
|     include_layered_windows: bool = False, | ||||
|     all_screens: bool = False, | ||||
|     xdisplay: str | None = None, | ||||
|     window: int | ImageWin.HWND | None = None, | ||||
| ) -> Image.Image: | ||||
|     im: Image.Image | ||||
|     if xdisplay is None: | ||||
|  | @ -51,8 +56,12 @@ def grab( | |||
|                 return im_resized | ||||
|             return im | ||||
|         elif sys.platform == "win32": | ||||
|             if window is not None: | ||||
|                 all_screens = -1 | ||||
|             offset, size, data = Image.core.grabscreen_win32( | ||||
|                 include_layered_windows, all_screens | ||||
|                 include_layered_windows, | ||||
|                 all_screens, | ||||
|                 int(window) if window is not None else 0, | ||||
|             ) | ||||
|             im = Image.frombytes( | ||||
|                 "RGB", | ||||
|  | @ -77,14 +86,16 @@ def grab( | |||
|             raise OSError(msg) | ||||
|         size, data = Image.core.grabscreen_x11(display_name) | ||||
|     except OSError: | ||||
|         if ( | ||||
|             display_name is None | ||||
|             and sys.platform not in ("darwin", "win32") | ||||
|             and shutil.which("gnome-screenshot") | ||||
|         ): | ||||
|         if display_name is None and sys.platform not in ("darwin", "win32"): | ||||
|             if shutil.which("gnome-screenshot"): | ||||
|                 args = ["gnome-screenshot", "-f"] | ||||
|             elif shutil.which("spectacle"): | ||||
|                 args = ["spectacle", "-n", "-b", "-f", "-o"] | ||||
|             else: | ||||
|                 raise | ||||
|             fh, filepath = tempfile.mkstemp(".png") | ||||
|             os.close(fh) | ||||
|             subprocess.call(["gnome-screenshot", "-f", filepath]) | ||||
|             subprocess.call(args + [filepath]) | ||||
|             im = Image.open(filepath) | ||||
|             im.load() | ||||
|             os.unlink(filepath) | ||||
|  |  | |||
|  | @ -409,8 +409,8 @@ class JpegImageFile(ImageFile.ImageFile): | |||
|         return super().__getstate__() + [self.layers, self.layer] | ||||
| 
 | ||||
|     def __setstate__(self, state: list[Any]) -> None: | ||||
|         self.layers, self.layer = state[6:] | ||||
|         super().__setstate__(state) | ||||
|         self.layers, self.layer = state[5:] | ||||
| 
 | ||||
|     def load_read(self, read_bytes: int) -> bytes: | ||||
|         """ | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ del _version | |||
| 
 | ||||
| 
 | ||||
| _plugins = [ | ||||
|     "AvifImagePlugin", | ||||
|     "BlpImagePlugin", | ||||
|     "BmpImagePlugin", | ||||
|     "BufrStubImagePlugin", | ||||
|  |  | |||
							
								
								
									
										3
									
								
								src/PIL/_avif.pyi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/PIL/_avif.pyi
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| from typing import Any | ||||
| 
 | ||||
| def __getattr__(name: str) -> Any: ... | ||||
|  | @ -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
									
								
							
							
						
						
									
										908
									
								
								src/_avif.c
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,908 @@ | |||
| #define PY_SSIZE_T_CLEAN | ||||
| 
 | ||||
| #include <Python.h> | ||||
| #include "avif/avif.h" | ||||
| 
 | ||||
| // Encoder type
 | ||||
| typedef struct { | ||||
|     PyObject_HEAD avifEncoder *encoder; | ||||
|     avifImage *image; | ||||
|     int first_frame; | ||||
| } AvifEncoderObject; | ||||
| 
 | ||||
| static PyTypeObject AvifEncoder_Type; | ||||
| 
 | ||||
| // Decoder type
 | ||||
| typedef struct { | ||||
|     PyObject_HEAD avifDecoder *decoder; | ||||
|     Py_buffer buffer; | ||||
| } AvifDecoderObject; | ||||
| 
 | ||||
| static PyTypeObject AvifDecoder_Type; | ||||
| 
 | ||||
| static int | ||||
| normalize_tiles_log2(int value) { | ||||
|     if (value < 0) { | ||||
|         return 0; | ||||
|     } else if (value > 6) { | ||||
|         return 6; | ||||
|     } else { | ||||
|         return value; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static PyObject * | ||||
| exc_type_for_avif_result(avifResult result) { | ||||
|     switch (result) { | ||||
|         case AVIF_RESULT_INVALID_EXIF_PAYLOAD: | ||||
|         case AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION: | ||||
|             return PyExc_ValueError; | ||||
|         case AVIF_RESULT_INVALID_FTYP: | ||||
|         case AVIF_RESULT_BMFF_PARSE_FAILED: | ||||
|         case AVIF_RESULT_TRUNCATED_DATA: | ||||
|         case AVIF_RESULT_NO_CONTENT: | ||||
|             return PyExc_SyntaxError; | ||||
|         default: | ||||
|             return PyExc_RuntimeError; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static uint8_t | ||||
| irot_imir_to_exif_orientation(const avifImage *image) { | ||||
|     uint8_t axis = image->imir.axis; | ||||
|     int imir = image->transformFlags & AVIF_TRANSFORM_IMIR; | ||||
|     int irot = image->transformFlags & AVIF_TRANSFORM_IROT; | ||||
|     if (irot) { | ||||
|         uint8_t angle = image->irot.angle; | ||||
|         if (angle == 1) { | ||||
|             if (imir) { | ||||
|                 return axis ? 7   // 90 degrees anti-clockwise then swap left and right.
 | ||||
|                             : 5;  // 90 degrees anti-clockwise then swap top and bottom.
 | ||||
|             } | ||||
|             return 6;  // 90 degrees anti-clockwise.
 | ||||
|         } | ||||
|         if (angle == 2) { | ||||
|             if (imir) { | ||||
|                 return axis | ||||
|                            ? 4   // 180 degrees anti-clockwise then swap left and right.
 | ||||
|                            : 2;  // 180 degrees anti-clockwise then swap top and bottom.
 | ||||
|             } | ||||
|             return 3;  // 180 degrees anti-clockwise.
 | ||||
|         } | ||||
|         if (angle == 3) { | ||||
|             if (imir) { | ||||
|                 return axis | ||||
|                            ? 5   // 270 degrees anti-clockwise then swap left and right.
 | ||||
|                            : 7;  // 270 degrees anti-clockwise then swap top and bottom.
 | ||||
|             } | ||||
|             return 8;  // 270 degrees anti-clockwise.
 | ||||
|         } | ||||
|     } | ||||
|     if (imir) { | ||||
|         return axis ? 2   // Swap left and right.
 | ||||
|                     : 4;  // Swap top and bottom.
 | ||||
|     } | ||||
|     return 1;  // Default orientation ("top-left", no-op).
 | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| exif_orientation_to_irot_imir(avifImage *image, int orientation) { | ||||
|     // Mapping from Exif orientation as defined in JEITA CP-3451C section 4.6.4.A
 | ||||
|     // Orientation to irot and imir boxes as defined in HEIF ISO/IEC 28002-12:2021
 | ||||
|     // sections 6.5.10 and 6.5.12.
 | ||||
|     switch (orientation) { | ||||
|         case 2:  // The 0th row is at the visual top of the image, and the 0th column is
 | ||||
|                  // the visual right-hand side.
 | ||||
|             image->transformFlags |= AVIF_TRANSFORM_IMIR; | ||||
|             image->imir.axis = 1; | ||||
|             break; | ||||
|         case 3:  // The 0th row is at the visual bottom of the image, and the 0th column
 | ||||
|                  // is the visual right-hand side.
 | ||||
|             image->transformFlags |= AVIF_TRANSFORM_IROT; | ||||
|             image->irot.angle = 2; | ||||
|             break; | ||||
|         case 4:  // The 0th row is at the visual bottom of the image, and the 0th column
 | ||||
|                  // is the visual left-hand side.
 | ||||
|             image->transformFlags |= AVIF_TRANSFORM_IMIR; | ||||
|             break; | ||||
|         case 5:  // The 0th row is the visual left-hand side of the image, and the 0th
 | ||||
|                  // column is the visual top.
 | ||||
|             image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; | ||||
|             image->irot.angle = 1;  // applied before imir according to MIAF spec
 | ||||
|                                     // ISO/IEC 28002-12:2021 - section 7.3.6.7
 | ||||
|             break; | ||||
|         case 6:  // The 0th row is the visual right-hand side of the image, and the 0th
 | ||||
|                  // column is the visual top.
 | ||||
|             image->transformFlags |= AVIF_TRANSFORM_IROT; | ||||
|             image->irot.angle = 3; | ||||
|             break; | ||||
|         case 7:  // The 0th row is the visual right-hand side of the image, and the 0th
 | ||||
|                  // column is the visual bottom.
 | ||||
|             image->transformFlags |= AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR; | ||||
|             image->irot.angle = 3;  // applied before imir according to MIAF spec
 | ||||
|                                     // ISO/IEC 28002-12:2021 - section 7.3.6.7
 | ||||
|             break; | ||||
|         case 8:  // The 0th row is the visual left-hand side of the image, and the 0th
 | ||||
|                  // column is the visual bottom.
 | ||||
|             image->transformFlags |= AVIF_TRANSFORM_IROT; | ||||
|             image->irot.angle = 1; | ||||
|             break; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| _codec_available(const char *name, avifCodecFlags flags) { | ||||
|     avifCodecChoice codec = avifCodecChoiceFromName(name); | ||||
|     if (codec == AVIF_CODEC_CHOICE_AUTO) { | ||||
|         return 0; | ||||
|     } | ||||
|     const char *codec_name = avifCodecName(codec, flags); | ||||
|     return (codec_name == NULL) ? 0 : 1; | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _decoder_codec_available(PyObject *self, PyObject *args) { | ||||
|     char *codec_name; | ||||
|     if (!PyArg_ParseTuple(args, "s", &codec_name)) { | ||||
|         return NULL; | ||||
|     } | ||||
|     int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_DECODE); | ||||
|     return PyBool_FromLong(is_available); | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _encoder_codec_available(PyObject *self, PyObject *args) { | ||||
|     char *codec_name; | ||||
|     if (!PyArg_ParseTuple(args, "s", &codec_name)) { | ||||
|         return NULL; | ||||
|     } | ||||
|     int is_available = _codec_available(codec_name, AVIF_CODEC_FLAG_CAN_ENCODE); | ||||
|     return PyBool_FromLong(is_available); | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _codec_versions(PyObject *self, PyObject *args) { | ||||
|     char buffer[256]; | ||||
|     avifCodecVersions(buffer); | ||||
|     return PyUnicode_FromString(buffer); | ||||
| } | ||||
| 
 | ||||
| static int | ||||
| _add_codec_specific_options(avifEncoder *encoder, PyObject *opts) { | ||||
|     Py_ssize_t i, size; | ||||
|     PyObject *keyval, *py_key, *py_val; | ||||
|     if (!PyTuple_Check(opts)) { | ||||
|         PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); | ||||
|         return 1; | ||||
|     } | ||||
|     size = PyTuple_GET_SIZE(opts); | ||||
| 
 | ||||
|     for (i = 0; i < size; i++) { | ||||
|         keyval = PyTuple_GetItem(opts, i); | ||||
|         if (!PyTuple_Check(keyval) || PyTuple_GET_SIZE(keyval) != 2) { | ||||
|             PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); | ||||
|             return 1; | ||||
|         } | ||||
|         py_key = PyTuple_GetItem(keyval, 0); | ||||
|         py_val = PyTuple_GetItem(keyval, 1); | ||||
|         if (!PyUnicode_Check(py_key) || !PyUnicode_Check(py_val)) { | ||||
|             PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); | ||||
|             return 1; | ||||
|         } | ||||
|         const char *key = PyUnicode_AsUTF8(py_key); | ||||
|         const char *val = PyUnicode_AsUTF8(py_val); | ||||
|         if (key == NULL || val == NULL) { | ||||
|             PyErr_SetString(PyExc_ValueError, "Invalid advanced codec options"); | ||||
|             return 1; | ||||
|         } | ||||
| 
 | ||||
|         avifResult result = avifEncoderSetCodecSpecificOption(encoder, key, val); | ||||
|         if (result != AVIF_RESULT_OK) { | ||||
|             PyErr_Format( | ||||
|                 exc_type_for_avif_result(result), | ||||
|                 "Setting advanced codec options failed: %s", | ||||
|                 avifResultToString(result) | ||||
|             ); | ||||
|             return 1; | ||||
|         } | ||||
|     } | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| // Encoder functions
 | ||||
| PyObject * | ||||
| AvifEncoderNew(PyObject *self_, PyObject *args) { | ||||
|     unsigned int width, height; | ||||
|     AvifEncoderObject *self = NULL; | ||||
|     avifEncoder *encoder = NULL; | ||||
| 
 | ||||
|     char *subsampling; | ||||
|     int quality; | ||||
|     int speed; | ||||
|     int exif_orientation; | ||||
|     int max_threads; | ||||
|     Py_buffer icc_buffer; | ||||
|     Py_buffer exif_buffer; | ||||
|     Py_buffer xmp_buffer; | ||||
|     int alpha_premultiplied; | ||||
|     int autotiling; | ||||
|     int tile_rows_log2; | ||||
|     int tile_cols_log2; | ||||
| 
 | ||||
|     char *codec; | ||||
|     char *range; | ||||
| 
 | ||||
|     PyObject *advanced; | ||||
|     int error = 0; | ||||
| 
 | ||||
|     if (!PyArg_ParseTuple( | ||||
|             args, | ||||
|             "(II)siiissiippy*y*iy*O", | ||||
|             &width, | ||||
|             &height, | ||||
|             &subsampling, | ||||
|             &quality, | ||||
|             &speed, | ||||
|             &max_threads, | ||||
|             &codec, | ||||
|             &range, | ||||
|             &tile_rows_log2, | ||||
|             &tile_cols_log2, | ||||
|             &alpha_premultiplied, | ||||
|             &autotiling, | ||||
|             &icc_buffer, | ||||
|             &exif_buffer, | ||||
|             &exif_orientation, | ||||
|             &xmp_buffer, | ||||
|             &advanced | ||||
|         )) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     // Create a new animation encoder and picture frame
 | ||||
|     avifImage *image = avifImageCreateEmpty(); | ||||
|     if (image == NULL) { | ||||
|         PyErr_SetString(PyExc_ValueError, "Image creation failed"); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
|     // Set these in advance so any upcoming RGB -> YUV use the proper coefficients
 | ||||
|     if (strcmp(range, "full") == 0) { | ||||
|         image->yuvRange = AVIF_RANGE_FULL; | ||||
|     } else if (strcmp(range, "limited") == 0) { | ||||
|         image->yuvRange = AVIF_RANGE_LIMITED; | ||||
|     } else { | ||||
|         PyErr_SetString(PyExc_ValueError, "Invalid range"); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
|     if (strcmp(subsampling, "4:0:0") == 0) { | ||||
|         image->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; | ||||
|     } else if (strcmp(subsampling, "4:2:0") == 0) { | ||||
|         image->yuvFormat = AVIF_PIXEL_FORMAT_YUV420; | ||||
|     } else if (strcmp(subsampling, "4:2:2") == 0) { | ||||
|         image->yuvFormat = AVIF_PIXEL_FORMAT_YUV422; | ||||
|     } else if (strcmp(subsampling, "4:4:4") == 0) { | ||||
|         image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; | ||||
|     } else { | ||||
|         PyErr_Format(PyExc_ValueError, "Invalid subsampling: %s", subsampling); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
|     // Validate canvas dimensions
 | ||||
|     if (width == 0 || height == 0) { | ||||
|         PyErr_SetString(PyExc_ValueError, "invalid canvas dimensions"); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
|     image->width = width; | ||||
|     image->height = height; | ||||
| 
 | ||||
|     image->depth = 8; | ||||
|     image->alphaPremultiplied = alpha_premultiplied ? AVIF_TRUE : AVIF_FALSE; | ||||
| 
 | ||||
|     encoder = avifEncoderCreate(); | ||||
|     if (!encoder) { | ||||
|         PyErr_SetString(PyExc_MemoryError, "Can't allocate encoder"); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
|     int is_aom_encode = strcmp(codec, "aom") == 0 || | ||||
|                         (strcmp(codec, "auto") == 0 && | ||||
|                          _codec_available("aom", AVIF_CODEC_FLAG_CAN_ENCODE)); | ||||
|     encoder->maxThreads = is_aom_encode && max_threads > 64 ? 64 : max_threads; | ||||
| 
 | ||||
|     encoder->quality = quality; | ||||
| 
 | ||||
|     if (strcmp(codec, "auto") == 0) { | ||||
|         encoder->codecChoice = AVIF_CODEC_CHOICE_AUTO; | ||||
|     } else { | ||||
|         encoder->codecChoice = avifCodecChoiceFromName(codec); | ||||
|     } | ||||
|     if (speed < AVIF_SPEED_SLOWEST) { | ||||
|         speed = AVIF_SPEED_SLOWEST; | ||||
|     } else if (speed > AVIF_SPEED_FASTEST) { | ||||
|         speed = AVIF_SPEED_FASTEST; | ||||
|     } | ||||
|     encoder->speed = speed; | ||||
|     encoder->timescale = (uint64_t)1000; | ||||
| 
 | ||||
|     encoder->autoTiling = autotiling ? AVIF_TRUE : AVIF_FALSE; | ||||
|     if (!autotiling) { | ||||
|         encoder->tileRowsLog2 = normalize_tiles_log2(tile_rows_log2); | ||||
|         encoder->tileColsLog2 = normalize_tiles_log2(tile_cols_log2); | ||||
|     } | ||||
| 
 | ||||
|     if (advanced != Py_None && _add_codec_specific_options(encoder, advanced)) { | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
|     self = PyObject_New(AvifEncoderObject, &AvifEncoder_Type); | ||||
|     if (!self) { | ||||
|         PyErr_SetString(PyExc_RuntimeError, "could not create encoder object"); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
|     self->first_frame = 1; | ||||
| 
 | ||||
|     avifResult result; | ||||
|     if (icc_buffer.len) { | ||||
|         result = avifImageSetProfileICC(image, icc_buffer.buf, icc_buffer.len); | ||||
|         if (result != AVIF_RESULT_OK) { | ||||
|             PyErr_Format( | ||||
|                 exc_type_for_avif_result(result), | ||||
|                 "Setting ICC profile failed: %s", | ||||
|                 avifResultToString(result) | ||||
|             ); | ||||
|             error = 1; | ||||
|             goto end; | ||||
|         } | ||||
|         // colorPrimaries and transferCharacteristics are ignored when an ICC
 | ||||
|         // profile is present, so set them to UNSPECIFIED.
 | ||||
|         image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; | ||||
|         image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; | ||||
|     } else { | ||||
|         image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; | ||||
|         image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; | ||||
|     } | ||||
|     image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; | ||||
| 
 | ||||
|     if (exif_buffer.len) { | ||||
|         result = avifImageSetMetadataExif(image, exif_buffer.buf, exif_buffer.len); | ||||
|         if (result != AVIF_RESULT_OK) { | ||||
|             PyErr_Format( | ||||
|                 exc_type_for_avif_result(result), | ||||
|                 "Setting EXIF data failed: %s", | ||||
|                 avifResultToString(result) | ||||
|             ); | ||||
|             error = 1; | ||||
|             goto end; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (xmp_buffer.len) { | ||||
|         result = avifImageSetMetadataXMP(image, xmp_buffer.buf, xmp_buffer.len); | ||||
|         if (result != AVIF_RESULT_OK) { | ||||
|             PyErr_Format( | ||||
|                 exc_type_for_avif_result(result), | ||||
|                 "Setting XMP data failed: %s", | ||||
|                 avifResultToString(result) | ||||
|             ); | ||||
|             error = 1; | ||||
|             goto end; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (exif_orientation > 1) { | ||||
|         exif_orientation_to_irot_imir(image, exif_orientation); | ||||
|     } | ||||
| 
 | ||||
|     self->image = image; | ||||
|     self->encoder = encoder; | ||||
| 
 | ||||
| end: | ||||
|     PyBuffer_Release(&icc_buffer); | ||||
|     PyBuffer_Release(&exif_buffer); | ||||
|     PyBuffer_Release(&xmp_buffer); | ||||
| 
 | ||||
|     if (error) { | ||||
|         if (image) { | ||||
|             avifImageDestroy(image); | ||||
|         } | ||||
|         if (encoder) { | ||||
|             avifEncoderDestroy(encoder); | ||||
|         } | ||||
|         if (self) { | ||||
|             PyObject_Del(self); | ||||
|         } | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     return (PyObject *)self; | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _encoder_dealloc(AvifEncoderObject *self) { | ||||
|     if (self->encoder) { | ||||
|         avifEncoderDestroy(self->encoder); | ||||
|     } | ||||
|     if (self->image) { | ||||
|         avifImageDestroy(self->image); | ||||
|     } | ||||
|     Py_RETURN_NONE; | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _encoder_add(AvifEncoderObject *self, PyObject *args) { | ||||
|     uint8_t *rgb_bytes; | ||||
|     Py_ssize_t size; | ||||
|     unsigned int duration; | ||||
|     unsigned int width; | ||||
|     unsigned int height; | ||||
|     char *mode; | ||||
|     unsigned int is_single_frame; | ||||
|     int error = 0; | ||||
| 
 | ||||
|     avifRGBImage rgb; | ||||
|     avifResult result; | ||||
| 
 | ||||
|     avifEncoder *encoder = self->encoder; | ||||
|     avifImage *image = self->image; | ||||
|     avifImage *frame = NULL; | ||||
| 
 | ||||
|     if (!PyArg_ParseTuple( | ||||
|             args, | ||||
|             "y#I(II)sp", | ||||
|             (char **)&rgb_bytes, | ||||
|             &size, | ||||
|             &duration, | ||||
|             &width, | ||||
|             &height, | ||||
|             &mode, | ||||
|             &is_single_frame | ||||
|         )) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     if (image->width != width || image->height != height) { | ||||
|         PyErr_Format( | ||||
|             PyExc_ValueError, | ||||
|             "Image sequence dimensions mismatch, %ux%u != %ux%u", | ||||
|             image->width, | ||||
|             image->height, | ||||
|             width, | ||||
|             height | ||||
|         ); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     if (self->first_frame) { | ||||
|         // If we don't have an image populated with yuv planes, this is the first frame
 | ||||
|         frame = image; | ||||
|     } else { | ||||
|         frame = avifImageCreateEmpty(); | ||||
|         if (image == NULL) { | ||||
|             PyErr_SetString(PyExc_ValueError, "Image creation failed"); | ||||
|             return NULL; | ||||
|         } | ||||
| 
 | ||||
|         frame->width = width; | ||||
|         frame->height = height; | ||||
|         frame->colorPrimaries = image->colorPrimaries; | ||||
|         frame->transferCharacteristics = image->transferCharacteristics; | ||||
|         frame->matrixCoefficients = image->matrixCoefficients; | ||||
|         frame->yuvRange = image->yuvRange; | ||||
|         frame->yuvFormat = image->yuvFormat; | ||||
|         frame->depth = image->depth; | ||||
|         frame->alphaPremultiplied = image->alphaPremultiplied; | ||||
|     } | ||||
| 
 | ||||
|     avifRGBImageSetDefaults(&rgb, frame); | ||||
| 
 | ||||
|     if (strcmp(mode, "RGBA") == 0) { | ||||
|         rgb.format = AVIF_RGB_FORMAT_RGBA; | ||||
|     } else { | ||||
|         rgb.format = AVIF_RGB_FORMAT_RGB; | ||||
|     } | ||||
| 
 | ||||
|     result = avifRGBImageAllocatePixels(&rgb); | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Pixel allocation failed: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
|     if (rgb.rowBytes * rgb.height != size) { | ||||
|         PyErr_Format( | ||||
|             PyExc_RuntimeError, | ||||
|             "rgb data has incorrect size: %u * %u (%u) != %u", | ||||
|             rgb.rowBytes, | ||||
|             rgb.height, | ||||
|             rgb.rowBytes * rgb.height, | ||||
|             size | ||||
|         ); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
|     // rgb.pixels is safe for writes
 | ||||
|     memcpy(rgb.pixels, rgb_bytes, size); | ||||
| 
 | ||||
|     Py_BEGIN_ALLOW_THREADS; | ||||
|     result = avifImageRGBToYUV(frame, &rgb); | ||||
|     Py_END_ALLOW_THREADS; | ||||
| 
 | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Conversion to YUV failed: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
|     uint32_t addImageFlags = | ||||
|         is_single_frame ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE; | ||||
| 
 | ||||
|     Py_BEGIN_ALLOW_THREADS; | ||||
|     result = avifEncoderAddImage(encoder, frame, duration, addImageFlags); | ||||
|     Py_END_ALLOW_THREADS; | ||||
| 
 | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Failed to encode image: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         error = 1; | ||||
|         goto end; | ||||
|     } | ||||
| 
 | ||||
| end: | ||||
|     if (&rgb) { | ||||
|         avifRGBImageFreePixels(&rgb); | ||||
|     } | ||||
|     if (!self->first_frame) { | ||||
|         avifImageDestroy(frame); | ||||
|     } | ||||
| 
 | ||||
|     if (error) { | ||||
|         return NULL; | ||||
|     } | ||||
|     self->first_frame = 0; | ||||
|     Py_RETURN_NONE; | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _encoder_finish(AvifEncoderObject *self) { | ||||
|     avifEncoder *encoder = self->encoder; | ||||
| 
 | ||||
|     avifRWData raw = AVIF_DATA_EMPTY; | ||||
|     avifResult result; | ||||
|     PyObject *ret = NULL; | ||||
| 
 | ||||
|     Py_BEGIN_ALLOW_THREADS; | ||||
|     result = avifEncoderFinish(encoder, &raw); | ||||
|     Py_END_ALLOW_THREADS; | ||||
| 
 | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Failed to finish encoding: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         avifRWDataFree(&raw); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     ret = PyBytes_FromStringAndSize((char *)raw.data, raw.size); | ||||
| 
 | ||||
|     avifRWDataFree(&raw); | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| // Decoder functions
 | ||||
| PyObject * | ||||
| AvifDecoderNew(PyObject *self_, PyObject *args) { | ||||
|     Py_buffer buffer; | ||||
|     AvifDecoderObject *self = NULL; | ||||
|     avifDecoder *decoder; | ||||
| 
 | ||||
|     char *codec_str; | ||||
|     avifCodecChoice codec; | ||||
|     int max_threads; | ||||
| 
 | ||||
|     avifResult result; | ||||
| 
 | ||||
|     if (!PyArg_ParseTuple(args, "y*si", &buffer, &codec_str, &max_threads)) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     if (strcmp(codec_str, "auto") == 0) { | ||||
|         codec = AVIF_CODEC_CHOICE_AUTO; | ||||
|     } else { | ||||
|         codec = avifCodecChoiceFromName(codec_str); | ||||
|     } | ||||
| 
 | ||||
|     self = PyObject_New(AvifDecoderObject, &AvifDecoder_Type); | ||||
|     if (!self) { | ||||
|         PyErr_SetString(PyExc_RuntimeError, "could not create decoder object"); | ||||
|         PyBuffer_Release(&buffer); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     decoder = avifDecoderCreate(); | ||||
|     if (!decoder) { | ||||
|         PyErr_SetString(PyExc_MemoryError, "Can't allocate decoder"); | ||||
|         PyBuffer_Release(&buffer); | ||||
|         PyObject_Del(self); | ||||
|         return NULL; | ||||
|     } | ||||
|     decoder->maxThreads = max_threads; | ||||
|     // Turn off libavif's 'clap' (clean aperture) property validation.
 | ||||
|     decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; | ||||
|     // Allow the PixelInformationProperty ('pixi') to be missing in AV1 image
 | ||||
|     // items. libheif v1.11.0 and older does not add the 'pixi' item property to
 | ||||
|     // AV1 image items.
 | ||||
|     decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; | ||||
|     decoder->codecChoice = codec; | ||||
| 
 | ||||
|     result = avifDecoderSetIOMemory(decoder, buffer.buf, buffer.len); | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Setting IO memory failed: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         avifDecoderDestroy(decoder); | ||||
|         PyBuffer_Release(&buffer); | ||||
|         PyObject_Del(self); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     result = avifDecoderParse(decoder); | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Failed to decode image: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         avifDecoderDestroy(decoder); | ||||
|         PyBuffer_Release(&buffer); | ||||
|         PyObject_Del(self); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     self->decoder = decoder; | ||||
|     self->buffer = buffer; | ||||
| 
 | ||||
|     return (PyObject *)self; | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _decoder_dealloc(AvifDecoderObject *self) { | ||||
|     if (self->decoder) { | ||||
|         avifDecoderDestroy(self->decoder); | ||||
|     } | ||||
|     PyBuffer_Release(&self->buffer); | ||||
|     Py_RETURN_NONE; | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _decoder_get_info(AvifDecoderObject *self) { | ||||
|     avifDecoder *decoder = self->decoder; | ||||
|     avifImage *image = decoder->image; | ||||
| 
 | ||||
|     PyObject *icc = NULL; | ||||
|     PyObject *exif = NULL; | ||||
|     PyObject *xmp = NULL; | ||||
|     PyObject *ret = NULL; | ||||
| 
 | ||||
|     if (image->xmp.size) { | ||||
|         xmp = PyBytes_FromStringAndSize((const char *)image->xmp.data, image->xmp.size); | ||||
|     } | ||||
| 
 | ||||
|     if (image->exif.size) { | ||||
|         exif = | ||||
|             PyBytes_FromStringAndSize((const char *)image->exif.data, image->exif.size); | ||||
|     } | ||||
| 
 | ||||
|     if (image->icc.size) { | ||||
|         icc = PyBytes_FromStringAndSize((const char *)image->icc.data, image->icc.size); | ||||
|     } | ||||
| 
 | ||||
|     ret = Py_BuildValue( | ||||
|         "(II)IsSSIS", | ||||
|         image->width, | ||||
|         image->height, | ||||
|         decoder->imageCount, | ||||
|         decoder->alphaPresent ? "RGBA" : "RGB", | ||||
|         NULL == icc ? Py_None : icc, | ||||
|         NULL == exif ? Py_None : exif, | ||||
|         irot_imir_to_exif_orientation(image), | ||||
|         NULL == xmp ? Py_None : xmp | ||||
|     ); | ||||
| 
 | ||||
|     Py_XDECREF(xmp); | ||||
|     Py_XDECREF(exif); | ||||
|     Py_XDECREF(icc); | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| PyObject * | ||||
| _decoder_get_frame(AvifDecoderObject *self, PyObject *args) { | ||||
|     PyObject *bytes; | ||||
|     PyObject *ret; | ||||
|     Py_ssize_t size; | ||||
|     avifResult result; | ||||
|     avifRGBImage rgb; | ||||
|     avifDecoder *decoder; | ||||
|     avifImage *image; | ||||
|     uint32_t frame_index; | ||||
| 
 | ||||
|     decoder = self->decoder; | ||||
| 
 | ||||
|     if (!PyArg_ParseTuple(args, "I", &frame_index)) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     result = avifDecoderNthImage(decoder, frame_index); | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Failed to decode frame %u: %s", | ||||
|             frame_index, | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     image = decoder->image; | ||||
| 
 | ||||
|     avifRGBImageSetDefaults(&rgb, image); | ||||
| 
 | ||||
|     rgb.depth = 8; | ||||
|     rgb.format = decoder->alphaPresent ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; | ||||
| 
 | ||||
|     result = avifRGBImageAllocatePixels(&rgb); | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Pixel allocation failed: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     Py_BEGIN_ALLOW_THREADS; | ||||
|     result = avifImageYUVToRGB(image, &rgb); | ||||
|     Py_END_ALLOW_THREADS; | ||||
| 
 | ||||
|     if (result != AVIF_RESULT_OK) { | ||||
|         PyErr_Format( | ||||
|             exc_type_for_avif_result(result), | ||||
|             "Conversion from YUV failed: %s", | ||||
|             avifResultToString(result) | ||||
|         ); | ||||
|         avifRGBImageFreePixels(&rgb); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     if (rgb.height > PY_SSIZE_T_MAX / rgb.rowBytes) { | ||||
|         PyErr_SetString(PyExc_MemoryError, "Integer overflow in pixel size"); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     size = rgb.rowBytes * rgb.height; | ||||
| 
 | ||||
|     bytes = PyBytes_FromStringAndSize((char *)rgb.pixels, size); | ||||
|     avifRGBImageFreePixels(&rgb); | ||||
| 
 | ||||
|     ret = Py_BuildValue( | ||||
|         "SKKK", | ||||
|         bytes, | ||||
|         decoder->timescale, | ||||
|         decoder->imageTiming.ptsInTimescales, | ||||
|         decoder->imageTiming.durationInTimescales | ||||
|     ); | ||||
| 
 | ||||
|     Py_DECREF(bytes); | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| /* -------------------------------------------------------------------- */ | ||||
| /* Type Definitions                                                     */ | ||||
| /* -------------------------------------------------------------------- */ | ||||
| 
 | ||||
| // AvifEncoder methods
 | ||||
| static struct PyMethodDef _encoder_methods[] = { | ||||
|     {"add", (PyCFunction)_encoder_add, METH_VARARGS}, | ||||
|     {"finish", (PyCFunction)_encoder_finish, METH_NOARGS}, | ||||
|     {NULL, NULL} /* sentinel */ | ||||
| }; | ||||
| 
 | ||||
| // AvifEncoder type definition
 | ||||
| static PyTypeObject AvifEncoder_Type = { | ||||
|     PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifEncoder", | ||||
|     .tp_basicsize = sizeof(AvifEncoderObject), | ||||
|     .tp_dealloc = (destructor)_encoder_dealloc, | ||||
|     .tp_methods = _encoder_methods, | ||||
| }; | ||||
| 
 | ||||
| // AvifDecoder methods
 | ||||
| static struct PyMethodDef _decoder_methods[] = { | ||||
|     {"get_info", (PyCFunction)_decoder_get_info, METH_NOARGS}, | ||||
|     {"get_frame", (PyCFunction)_decoder_get_frame, METH_VARARGS}, | ||||
|     {NULL, NULL} /* sentinel */ | ||||
| }; | ||||
| 
 | ||||
| // AvifDecoder type definition
 | ||||
| static PyTypeObject AvifDecoder_Type = { | ||||
|     PyVarObject_HEAD_INIT(NULL, 0).tp_name = "AvifDecoder", | ||||
|     .tp_basicsize = sizeof(AvifDecoderObject), | ||||
|     .tp_dealloc = (destructor)_decoder_dealloc, | ||||
|     .tp_methods = _decoder_methods, | ||||
| }; | ||||
| 
 | ||||
| /* -------------------------------------------------------------------- */ | ||||
| /* Module Setup                                                         */ | ||||
| /* -------------------------------------------------------------------- */ | ||||
| 
 | ||||
| static PyMethodDef avifMethods[] = { | ||||
|     {"AvifDecoder", AvifDecoderNew, METH_VARARGS}, | ||||
|     {"AvifEncoder", AvifEncoderNew, METH_VARARGS}, | ||||
|     {"decoder_codec_available", _decoder_codec_available, METH_VARARGS}, | ||||
|     {"encoder_codec_available", _encoder_codec_available, METH_VARARGS}, | ||||
|     {"codec_versions", _codec_versions, METH_NOARGS}, | ||||
|     {NULL, NULL} | ||||
| }; | ||||
| 
 | ||||
| static int | ||||
| setup_module(PyObject *m) { | ||||
|     if (PyType_Ready(&AvifDecoder_Type) < 0 || PyType_Ready(&AvifEncoder_Type) < 0) { | ||||
|         return -1; | ||||
|     } | ||||
| 
 | ||||
|     PyObject *d = PyModule_GetDict(m); | ||||
|     PyObject *v = PyUnicode_FromString(avifVersion()); | ||||
|     PyDict_SetItemString(d, "libavif_version", v ? v : Py_None); | ||||
|     Py_XDECREF(v); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| PyMODINIT_FUNC | ||||
| PyInit__avif(void) { | ||||
|     PyObject *m; | ||||
| 
 | ||||
|     static PyModuleDef module_def = { | ||||
|         PyModuleDef_HEAD_INIT, | ||||
|         .m_name = "_avif", | ||||
|         .m_size = -1, | ||||
|         .m_methods = avifMethods, | ||||
|     }; | ||||
| 
 | ||||
|     m = PyModule_Create(&module_def); | ||||
|     if (setup_module(m) < 0) { | ||||
|         Py_DECREF(m); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
							
								
								
									
										115
									
								
								src/_imaging.c
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								src/_imaging.c
									
									
									
									
									
								
							|  | @ -230,6 +230,93 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { | |||
|     return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); | ||||
| } | ||||
| 
 | ||||
| /* -------------------------------------------------------------------- */ | ||||
| /* 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 */ | ||||
|  |  | |||
|  | @ -286,29 +286,42 @@ PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) { | |||
| /* -------------------------------------------------------------------- */ | ||||
| /* Windows screen grabber */ | ||||
| 
 | ||||
| typedef HANDLE(__stdcall *Func_GetWindowDpiAwarenessContext)(HANDLE); | ||||
| typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE); | ||||
| 
 | ||||
| PyObject * | ||||
| PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { | ||||
|     int x = 0, y = 0, width, height; | ||||
|     int includeLayeredWindows = 0, all_screens = 0; | ||||
|     int x = 0, y = 0, width = -1, height; | ||||
|     int includeLayeredWindows = 0, screens = 0; | ||||
|     HBITMAP bitmap; | ||||
|     BITMAPCOREHEADER core; | ||||
|     HDC screen, screen_copy; | ||||
|     HWND wnd; | ||||
|     DWORD rop; | ||||
|     PyObject *buffer; | ||||
|     HANDLE dpiAwareness; | ||||
|     HANDLE dpiAwareness = NULL; | ||||
|     HMODULE user32; | ||||
|     Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function; | ||||
|     Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; | ||||
| 
 | ||||
|     if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { | ||||
|     if (!PyArg_ParseTuple( | ||||
|             args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd | ||||
|         )) { | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     /* step 1: create a memory DC large enough to hold the
 | ||||
|        entire screen */ | ||||
| 
 | ||||
|     screen = CreateDC("DISPLAY", NULL, NULL, NULL); | ||||
|     if (screens == -1) { | ||||
|         screen = GetDC(wnd); | ||||
|         if (screen == NULL) { | ||||
|             PyErr_SetString(PyExc_OSError, "unable to get device context for handle"); | ||||
|             return NULL; | ||||
|         } | ||||
|     } else { | ||||
|         screen = CreateDC("DISPLAY", NULL, NULL, NULL); | ||||
|     } | ||||
|     screen_copy = CreateCompatibleDC(screen); | ||||
| 
 | ||||
|     // added in Windows 10 (1607)
 | ||||
|  | @ -317,15 +330,28 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { | |||
|     SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext | ||||
|     )GetProcAddress(user32, "SetThreadDpiAwarenessContext"); | ||||
|     if (SetThreadDpiAwarenessContext_function != NULL) { | ||||
|         GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext | ||||
|         )GetProcAddress(user32, "GetWindowDpiAwarenessContext"); | ||||
|         if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) { | ||||
|             dpiAwareness = GetWindowDpiAwarenessContext_function(wnd); | ||||
|         } | ||||
|         // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3)
 | ||||
|         dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); | ||||
|         dpiAwareness = SetThreadDpiAwarenessContext_function( | ||||
|             dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     if (all_screens) { | ||||
|     if (screens == 1) { | ||||
|         x = GetSystemMetrics(SM_XVIRTUALSCREEN); | ||||
|         y = GetSystemMetrics(SM_YVIRTUALSCREEN); | ||||
|         width = GetSystemMetrics(SM_CXVIRTUALSCREEN); | ||||
|         height = GetSystemMetrics(SM_CYVIRTUALSCREEN); | ||||
|     } else if (screens == -1) { | ||||
|         RECT rect; | ||||
|         if (GetClientRect(wnd, &rect)) { | ||||
|             width = rect.right; | ||||
|             height = rect.bottom; | ||||
|         } | ||||
|     } else { | ||||
|         width = GetDeviceCaps(screen, HORZRES); | ||||
|         height = GetDeviceCaps(screen, VERTRES); | ||||
|  | @ -337,6 +363,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { | |||
| 
 | ||||
|     FreeLibrary(user32); | ||||
| 
 | ||||
|     if (width == -1) { | ||||
|         goto error; | ||||
|     } | ||||
| 
 | ||||
|     bitmap = CreateCompatibleBitmap(screen, width, height); | ||||
|     if (!bitmap) { | ||||
|         goto error; | ||||
|  | @ -382,7 +412,11 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { | |||
| 
 | ||||
|     DeleteObject(bitmap); | ||||
|     DeleteDC(screen_copy); | ||||
|     DeleteDC(screen); | ||||
|     if (screens == -1) { | ||||
|         ReleaseDC(wnd, screen); | ||||
|     } else { | ||||
|         DeleteDC(screen); | ||||
|     } | ||||
| 
 | ||||
|     return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); | ||||
| 
 | ||||
|  | @ -390,7 +424,11 @@ error: | |||
|     PyErr_SetString(PyExc_OSError, "screen grab failed"); | ||||
| 
 | ||||
|     DeleteDC(screen_copy); | ||||
|     DeleteDC(screen); | ||||
|     if (screens == -1) { | ||||
|         ReleaseDC(wnd, screen); | ||||
|     } else { | ||||
|         DeleteDC(screen); | ||||
|     } | ||||
| 
 | ||||
|     return NULL; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										299
									
								
								src/libImaging/Arrow.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								src/libImaging/Arrow.c
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,299 @@ | |||
| 
 | ||||
| #include "Arrow.h" | ||||
| #include "Imaging.h" | ||||
| #include <string.h> | ||||
| 
 | ||||
| /* struct ArrowSchema* */ | ||||
| /* _arrow_schema_channel(char* channel, char* format) { */ | ||||
| 
 | ||||
| /* } */ | ||||
| 
 | ||||
| static void | ||||
| ReleaseExportedSchema(struct ArrowSchema *array) { | ||||
|     // This should not be called on already released array
 | ||||
|     // assert(array->release != NULL);
 | ||||
| 
 | ||||
|     if (!array->release) { | ||||
|         return; | ||||
|     } | ||||
|     if (array->format) { | ||||
|         free((void *)array->format); | ||||
|         array->format = NULL; | ||||
|     } | ||||
|     if (array->name) { | ||||
|         free((void *)array->name); | ||||
|         array->name = NULL; | ||||
|     } | ||||
|     if (array->metadata) { | ||||
|         free((void *)array->metadata); | ||||
|         array->metadata = NULL; | ||||
|     } | ||||
| 
 | ||||
|     // Release children
 | ||||
|     for (int64_t i = 0; i < array->n_children; ++i) { | ||||
|         struct ArrowSchema *child = array->children[i]; | ||||
|         if (child->release != NULL) { | ||||
|             child->release(child); | ||||
|             child->release = NULL; | ||||
|         } | ||||
|         // UNDONE -- should I be releasing the children?
 | ||||
|     } | ||||
| 
 | ||||
|     // Release dictionary
 | ||||
|     struct ArrowSchema *dict = array->dictionary; | ||||
|     if (dict != NULL && dict->release != NULL) { | ||||
|         dict->release(dict); | ||||
|         dict->release = NULL; | ||||
|     } | ||||
| 
 | ||||
|     // TODO here: release and/or deallocate all data directly owned by
 | ||||
|     // the ArrowArray struct, such as the private_data.
 | ||||
| 
 | ||||
|     // Mark array released
 | ||||
|     array->release = NULL; | ||||
| } | ||||
| 
 | ||||
| int | ||||
| export_named_type(struct ArrowSchema *schema, char *format, char *name) { | ||||
|     char *formatp; | ||||
|     char *namep; | ||||
|     size_t format_len = strlen(format) + 1; | ||||
|     size_t name_len = strlen(name) + 1; | ||||
| 
 | ||||
|     formatp = calloc(format_len, 1); | ||||
| 
 | ||||
|     if (!formatp) { | ||||
|         return IMAGING_CODEC_MEMORY; | ||||
|     } | ||||
| 
 | ||||
|     namep = calloc(name_len, 1); | ||||
|     if (!namep) { | ||||
|         free(formatp); | ||||
|         return IMAGING_CODEC_MEMORY; | ||||
|     } | ||||
| 
 | ||||
|     strncpy(formatp, format, format_len); | ||||
|     strncpy(namep, name, name_len); | ||||
| 
 | ||||
|     *schema = (struct ArrowSchema){// Type description
 | ||||
|                                    .format = formatp, | ||||
|                                    .name = namep, | ||||
|                                    .metadata = NULL, | ||||
|                                    .flags = 0, | ||||
|                                    .n_children = 0, | ||||
|                                    .children = NULL, | ||||
|                                    .dictionary = NULL, | ||||
|                                    // Bookkeeping
 | ||||
|                                    .release = &ReleaseExportedSchema | ||||
|     }; | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| int | ||||
| export_imaging_schema(Imaging im, struct ArrowSchema *schema) { | ||||
|     int retval = 0; | ||||
| 
 | ||||
|     if (strcmp(im->arrow_band_format, "") == 0) { | ||||
|         return IMAGING_ARROW_INCOMPATIBLE_MODE; | ||||
|     } | ||||
| 
 | ||||
|     /* for now, single block images */ | ||||
|     if (!(im->blocks_count == 0 || im->blocks_count == 1)) { | ||||
|         return IMAGING_ARROW_MEMORY_LAYOUT; | ||||
|     } | ||||
| 
 | ||||
|     if (im->bands == 1) { | ||||
|         return export_named_type(schema, im->arrow_band_format, im->band_names[0]); | ||||
|     } | ||||
| 
 | ||||
|     retval = export_named_type(schema, "+w:4", ""); | ||||
|     if (retval != 0) { | ||||
|         return retval; | ||||
|     } | ||||
|     // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands.
 | ||||
|     schema->n_children = 1; | ||||
|     schema->children = calloc(1, sizeof(struct ArrowSchema *)); | ||||
|     schema->children[0] = (struct ArrowSchema *)calloc(1, sizeof(struct ArrowSchema)); | ||||
|     retval = export_named_type(schema->children[0], im->arrow_band_format, "pixel"); | ||||
|     if (retval != 0) { | ||||
|         free(schema->children[0]); | ||||
|         schema->release(schema); | ||||
|         return retval; | ||||
|     } | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| static void | ||||
| release_const_array(struct ArrowArray *array) { | ||||
|     Imaging im = (Imaging)array->private_data; | ||||
| 
 | ||||
|     if (array->n_children == 0) { | ||||
|         ImagingDelete(im); | ||||
|     } | ||||
| 
 | ||||
|     //  Free the buffers and the buffers array
 | ||||
|     if (array->buffers) { | ||||
|         free(array->buffers); | ||||
|         array->buffers = NULL; | ||||
|     } | ||||
|     if (array->children) { | ||||
|         // undone -- does arrow release all the children recursively?
 | ||||
|         for (int i = 0; i < array->n_children; i++) { | ||||
|             if (array->children[i]->release) { | ||||
|                 array->children[i]->release(array->children[i]); | ||||
|                 array->children[i]->release = NULL; | ||||
|                 free(array->children[i]); | ||||
|             } | ||||
|         } | ||||
|         free(array->children); | ||||
|         array->children = NULL; | ||||
|     } | ||||
|     // Mark released
 | ||||
|     array->release = NULL; | ||||
| } | ||||
| 
 | ||||
| int | ||||
| export_single_channel_array(Imaging im, struct ArrowArray *array) { | ||||
|     int length = im->xsize * im->ysize; | ||||
| 
 | ||||
|     /* for now, single block images */ | ||||
|     if (!(im->blocks_count == 0 || im->blocks_count == 1)) { | ||||
|         return IMAGING_ARROW_MEMORY_LAYOUT; | ||||
|     } | ||||
| 
 | ||||
|     if (im->lines_per_block && im->lines_per_block < im->ysize) { | ||||
|         length = im->xsize * im->lines_per_block; | ||||
|     } | ||||
| 
 | ||||
|     MUTEX_LOCK(&im->mutex); | ||||
|     im->refcount++; | ||||
|     MUTEX_UNLOCK(&im->mutex); | ||||
|     // Initialize primitive fields
 | ||||
|     *array = (struct ArrowArray){// Data description
 | ||||
|                                  .length = length, | ||||
|                                  .offset = 0, | ||||
|                                  .null_count = 0, | ||||
|                                  .n_buffers = 2, | ||||
|                                  .n_children = 0, | ||||
|                                  .children = NULL, | ||||
|                                  .dictionary = NULL, | ||||
|                                  // Bookkeeping
 | ||||
|                                  .release = &release_const_array, | ||||
|                                  .private_data = im | ||||
|     }; | ||||
| 
 | ||||
|     // Allocate list of buffers
 | ||||
|     array->buffers = (const void **)malloc(sizeof(void *) * array->n_buffers); | ||||
|     // assert(array->buffers != NULL);
 | ||||
|     array->buffers[0] = NULL;  // no nulls, null bitmap can be omitted
 | ||||
| 
 | ||||
|     if (im->block) { | ||||
|         array->buffers[1] = im->block; | ||||
|     } else { | ||||
|         array->buffers[1] = im->blocks[0].ptr; | ||||
|     } | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| int | ||||
| export_fixed_pixel_array(Imaging im, struct ArrowArray *array) { | ||||
|     int length = im->xsize * im->ysize; | ||||
| 
 | ||||
|     /* for now, single block images */ | ||||
|     if (!(im->blocks_count == 0 || im->blocks_count == 1)) { | ||||
|         return IMAGING_ARROW_MEMORY_LAYOUT; | ||||
|     } | ||||
| 
 | ||||
|     if (im->lines_per_block && im->lines_per_block < im->ysize) { | ||||
|         length = im->xsize * im->lines_per_block; | ||||
|     } | ||||
| 
 | ||||
|     MUTEX_LOCK(&im->mutex); | ||||
|     im->refcount++; | ||||
|     MUTEX_UNLOCK(&im->mutex); | ||||
|     // Initialize primitive fields
 | ||||
|     // Fixed length arrays are 1 buffer of validity, and the length in pixels.
 | ||||
|     // Data is in a child array.
 | ||||
|     *array = (struct ArrowArray){// Data description
 | ||||
|                                  .length = length, | ||||
|                                  .offset = 0, | ||||
|                                  .null_count = 0, | ||||
|                                  .n_buffers = 1, | ||||
|                                  .n_children = 1, | ||||
|                                  .children = NULL, | ||||
|                                  .dictionary = NULL, | ||||
|                                  // Bookkeeping
 | ||||
|                                  .release = &release_const_array, | ||||
|                                  .private_data = im | ||||
|     }; | ||||
| 
 | ||||
|     // Allocate list of buffers
 | ||||
|     array->buffers = (const void **)calloc(1, sizeof(void *) * array->n_buffers); | ||||
|     if (!array->buffers) { | ||||
|         goto err; | ||||
|     } | ||||
|     // assert(array->buffers != NULL);
 | ||||
|     array->buffers[0] = NULL;  // no nulls, null bitmap can be omitted
 | ||||
| 
 | ||||
|     // if it's not 1 band, it's an int32 at the moment. 4 uint8 bands.
 | ||||
|     array->n_children = 1; | ||||
|     array->children = calloc(1, sizeof(struct ArrowArray *)); | ||||
|     if (!array->children) { | ||||
|         goto err; | ||||
|     } | ||||
|     array->children[0] = (struct ArrowArray *)calloc(1, sizeof(struct ArrowArray)); | ||||
|     if (!array->children[0]) { | ||||
|         goto err; | ||||
|     } | ||||
| 
 | ||||
|     MUTEX_LOCK(&im->mutex); | ||||
|     im->refcount++; | ||||
|     MUTEX_UNLOCK(&im->mutex); | ||||
|     *array->children[0] = (struct ArrowArray){// Data description
 | ||||
|                                               .length = length * 4, | ||||
|                                               .offset = 0, | ||||
|                                               .null_count = 0, | ||||
|                                               .n_buffers = 2, | ||||
|                                               .n_children = 0, | ||||
|                                               .children = NULL, | ||||
|                                               .dictionary = NULL, | ||||
|                                               // Bookkeeping
 | ||||
|                                               .release = &release_const_array, | ||||
|                                               .private_data = im | ||||
|     }; | ||||
| 
 | ||||
|     array->children[0]->buffers = | ||||
|         (const void **)calloc(2, sizeof(void *) * array->n_buffers); | ||||
| 
 | ||||
|     if (im->block) { | ||||
|         array->children[0]->buffers[1] = im->block; | ||||
|     } else { | ||||
|         array->children[0]->buffers[1] = im->blocks[0].ptr; | ||||
|     } | ||||
|     return 0; | ||||
| 
 | ||||
| err: | ||||
|     if (array->children[0]) { | ||||
|         free(array->children[0]); | ||||
|     } | ||||
|     if (array->children) { | ||||
|         free(array->children); | ||||
|     } | ||||
|     if (array->buffers) { | ||||
|         free(array->buffers); | ||||
|     } | ||||
|     return IMAGING_CODEC_MEMORY; | ||||
| } | ||||
| 
 | ||||
| int | ||||
| export_imaging_array(Imaging im, struct ArrowArray *array) { | ||||
|     if (strcmp(im->arrow_band_format, "") == 0) { | ||||
|         return IMAGING_ARROW_INCOMPATIBLE_MODE; | ||||
|     } | ||||
| 
 | ||||
|     if (im->bands == 1) { | ||||
|         return export_single_channel_array(im, array); | ||||
|     } | ||||
| 
 | ||||
|     return export_fixed_pixel_array(im, array); | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/libImaging/Arrow.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/libImaging/Arrow.h
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| #include <stdint.h> | ||||
| #include <assert.h> | ||||
| 
 | ||||
| // Apache License 2.0.
 | ||||
| // Source apache arrow project
 | ||||
| // https://arrow.apache.org/docs/format/CDataInterface.html
 | ||||
| 
 | ||||
| #ifndef ARROW_C_DATA_INTERFACE | ||||
| #define ARROW_C_DATA_INTERFACE | ||||
| 
 | ||||
| #define ARROW_FLAG_DICTIONARY_ORDERED 1 | ||||
| #define ARROW_FLAG_NULLABLE 2 | ||||
| #define ARROW_FLAG_MAP_KEYS_SORTED 4 | ||||
| 
 | ||||
| struct ArrowSchema { | ||||
|     // Array type description
 | ||||
|     const char *format; | ||||
|     const char *name; | ||||
|     const char *metadata; | ||||
|     int64_t flags; | ||||
|     int64_t n_children; | ||||
|     struct ArrowSchema **children; | ||||
|     struct ArrowSchema *dictionary; | ||||
| 
 | ||||
|     // Release callback
 | ||||
|     void (*release)(struct ArrowSchema *); | ||||
|     // Opaque producer-specific data
 | ||||
|     void *private_data; | ||||
| }; | ||||
| 
 | ||||
| struct ArrowArray { | ||||
|     // Array data description
 | ||||
|     int64_t length; | ||||
|     int64_t null_count; | ||||
|     int64_t offset; | ||||
|     int64_t n_buffers; | ||||
|     int64_t n_children; | ||||
|     const void **buffers; | ||||
|     struct ArrowArray **children; | ||||
|     struct ArrowArray *dictionary; | ||||
| 
 | ||||
|     // Release callback
 | ||||
|     void (*release)(struct ArrowArray *); | ||||
|     // Opaque producer-specific data
 | ||||
|     void *private_data; | ||||
| }; | ||||
| 
 | ||||
| #endif  // ARROW_C_DATA_INTERFACE
 | ||||
|  | @ -20,6 +20,8 @@ extern "C" { | |||
| #define M_PI 3.1415926535897932384626433832795 | ||||
| #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; | ||||
|  |  | |||
|  | @ -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 */ | ||||
|  |  | |||
							
								
								
									
										26
									
								
								wheels/dependency_licenses/AOM.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								wheels/dependency_licenses/AOM.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| Copyright (c) 2016, Alliance for Open Media. All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions | ||||
| are met: | ||||
| 
 | ||||
| 1. Redistributions of source code must retain the above copyright | ||||
|    notice, this list of conditions and the following disclaimer. | ||||
| 
 | ||||
| 2. Redistributions in binary form must reproduce the above copyright | ||||
|    notice, this list of conditions and the following disclaimer in | ||||
|    the documentation and/or other materials provided with the | ||||
|    distribution. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | ||||
| FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | ||||
| COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | ||||
| INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | ||||
| BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | ||||
| LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | ||||
| ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||||
| POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										23
									
								
								wheels/dependency_licenses/DAV1D.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								wheels/dependency_licenses/DAV1D.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| Copyright © 2018-2019, VideoLAN and dav1d authors | ||||
| All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
| 1. Redistributions of source code must retain the above copyright notice, this | ||||
|    list of conditions and the following disclaimer. | ||||
| 
 | ||||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|    this list of conditions and the following disclaimer in the documentation | ||||
|    and/or other materials provided with the distribution. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||||
| ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||||
| WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||||
| ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||
| (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||||
| ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										387
									
								
								wheels/dependency_licenses/LIBAVIF.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										387
									
								
								wheels/dependency_licenses/LIBAVIF.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,387 @@ | |||
| Copyright 2019 Joe Drago. All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
| 1. Redistributions of source code must retain the above copyright notice, this | ||||
| list of conditions and the following disclaimer. | ||||
| 
 | ||||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
| this list of conditions and the following disclaimer in the documentation | ||||
| and/or other materials provided with the distribution. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| 
 | ||||
| Files: src/obu.c | ||||
| 
 | ||||
| Copyright © 2018-2019, VideoLAN and dav1d authors | ||||
| All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
| 1. Redistributions of source code must retain the above copyright notice, this | ||||
|    list of conditions and the following disclaimer. | ||||
| 
 | ||||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|    this list of conditions and the following disclaimer in the documentation | ||||
|    and/or other materials provided with the distribution. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||||
| ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||||
| WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||||
| ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||
| (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||
| LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||||
| ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| 
 | ||||
| Files: third_party/iccjpeg/* | ||||
| 
 | ||||
| In plain English: | ||||
| 
 | ||||
| 1. We don't promise that this software works.  (But if you find any bugs, | ||||
|    please let us know!) | ||||
| 2. You can use this software for whatever you want.  You don't have to pay us. | ||||
| 3. You may not pretend that you wrote this software.  If you use it in a | ||||
|    program, you must acknowledge somewhere in your documentation that | ||||
|    you've used the IJG code. | ||||
| 
 | ||||
| In legalese: | ||||
| 
 | ||||
| The authors make NO WARRANTY or representation, either express or implied, | ||||
| with respect to this software, its quality, accuracy, merchantability, or | ||||
| fitness for a particular purpose.  This software is provided "AS IS", and you, | ||||
| its user, assume the entire risk as to its quality and accuracy. | ||||
| 
 | ||||
| This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. | ||||
| All Rights Reserved except as specified below. | ||||
| 
 | ||||
| Permission is hereby granted to use, copy, modify, and distribute this | ||||
| software (or portions thereof) for any purpose, without fee, subject to these | ||||
| conditions: | ||||
| (1) If any part of the source code for this software is distributed, then this | ||||
| README file must be included, with this copyright and no-warranty notice | ||||
| unaltered; and any additions, deletions, or changes to the original files | ||||
| must be clearly indicated in accompanying documentation. | ||||
| (2) If only executable code is distributed, then the accompanying | ||||
| documentation must state that "this software is based in part on the work of | ||||
| the Independent JPEG Group". | ||||
| (3) Permission for use of this software is granted only if the user accepts | ||||
| full responsibility for any undesirable consequences; the authors accept | ||||
| NO LIABILITY for damages of any kind. | ||||
| 
 | ||||
| These conditions apply to any software derived from or based on the IJG code, | ||||
| not just to the unmodified library.  If you use our work, you ought to | ||||
| acknowledge us. | ||||
| 
 | ||||
| Permission is NOT granted for the use of any IJG author's name or company name | ||||
| in advertising or publicity relating to this software or products derived from | ||||
| it.  This software may be referred to only as "the Independent JPEG Group's | ||||
| software". | ||||
| 
 | ||||
| We specifically permit and encourage the use of this software as the basis of | ||||
| commercial products, provided that all warranty or liability claims are | ||||
| assumed by the product vendor. | ||||
| 
 | ||||
| 
 | ||||
| The Unix configuration script "configure" was produced with GNU Autoconf. | ||||
| It is copyright by the Free Software Foundation but is freely distributable. | ||||
| The same holds for its supporting scripts (config.guess, config.sub, | ||||
| ltmain.sh).  Another support script, install-sh, is copyright by X Consortium | ||||
| but is also freely distributable. | ||||
| 
 | ||||
| The IJG distribution formerly included code to read and write GIF files. | ||||
| To avoid entanglement with the Unisys LZW patent, GIF reading support has | ||||
| been removed altogether, and the GIF writer has been simplified to produce | ||||
| "uncompressed GIFs".  This technique does not use the LZW algorithm; the | ||||
| resulting GIF files are larger than usual, but are readable by all standard | ||||
| GIF decoders. | ||||
| 
 | ||||
| We are required to state that | ||||
|     "The Graphics Interchange Format(c) is the Copyright property of | ||||
|     CompuServe Incorporated.  GIF(sm) is a Service Mark property of | ||||
|     CompuServe Incorporated." | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| 
 | ||||
| Files: contrib/gdk-pixbuf/* | ||||
| 
 | ||||
| Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
| 1. Redistributions of source code must retain the above copyright notice, this | ||||
| list of conditions and the following disclaimer. | ||||
| 
 | ||||
| 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
| this list of conditions and the following disclaimer in the documentation | ||||
| and/or other materials provided with the distribution. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| 
 | ||||
| Files: android_jni/gradlew* | ||||
| 
 | ||||
| 
 | ||||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
| 
 | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
| 
 | ||||
|    1. Definitions. | ||||
| 
 | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
| 
 | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
| 
 | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
| 
 | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
| 
 | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
| 
 | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
| 
 | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
| 
 | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
| 
 | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
| 
 | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
| 
 | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
| 
 | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
| 
 | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
| 
 | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
| 
 | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
| 
 | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
| 
 | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
| 
 | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
| 
 | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
| 
 | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
| 
 | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
| 
 | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
| 
 | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
| 
 | ||||
|    END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
| 
 | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
| 
 | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| 
 | ||||
| ------------------------------------------------------------------------------ | ||||
| 
 | ||||
| Files: third_party/libyuv/* | ||||
| 
 | ||||
| Copyright 2011 The LibYuv Project Authors. All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are | ||||
| met: | ||||
| 
 | ||||
|   * Redistributions of source code must retain the above copyright | ||||
|     notice, this list of conditions and the following disclaimer. | ||||
| 
 | ||||
|   * Redistributions in binary form must reproduce the above copyright | ||||
|     notice, this list of conditions and the following disclaimer in | ||||
|     the documentation and/or other materials provided with the | ||||
|     distribution. | ||||
| 
 | ||||
|   * Neither the name of Google nor the names of its contributors may | ||||
|     be used to endorse or promote products derived from this software | ||||
|     without specific prior written permission. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										29
									
								
								wheels/dependency_licenses/LIBYUV.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								wheels/dependency_licenses/LIBYUV.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| Copyright 2011 The LibYuv Project Authors. All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are | ||||
| met: | ||||
| 
 | ||||
|   * Redistributions of source code must retain the above copyright | ||||
|     notice, this list of conditions and the following disclaimer. | ||||
| 
 | ||||
|   * Redistributions in binary form must reproduce the above copyright | ||||
|     notice, this list of conditions and the following disclaimer in | ||||
|     the documentation and/or other materials provided with the | ||||
|     distribution. | ||||
| 
 | ||||
|   * Neither the name of Google nor the names of its contributors may | ||||
|     be used to endorse or promote products derived from this software | ||||
|     without specific prior written permission. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
							
								
								
									
										25
									
								
								wheels/dependency_licenses/RAV1E.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								wheels/dependency_licenses/RAV1E.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| BSD 2-Clause License | ||||
| 
 | ||||
| Copyright (c) 2017-2023, the rav1e contributors | ||||
| 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. | ||||
| 
 | ||||
| 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. | ||||
							
								
								
									
										26
									
								
								wheels/dependency_licenses/SVT-AV1.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								wheels/dependency_licenses/SVT-AV1.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| Copyright (c) 2019, 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. | ||||
|  | @ -61,6 +61,7 @@ Run ``build_prepare.py`` to configure the build:: | |||
|       --no-imagequant       skip GPL-licensed optional dependency libimagequant | ||||
|       --no-fribidi, --no-raqm | ||||
|                             skip LGPL-licensed optional dependency FriBiDi | ||||
|       --no-avif             skip optional dependency libavif | ||||
| 
 | ||||
|     Arguments can also be supplied using the environment variables PILLOW_BUILD, | ||||
|     PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. | ||||
|  |  | |||
|  | @ -116,6 +116,7 @@ V = { | |||
|     "HARFBUZZ": "11.0.0", | ||||
|     "JPEGTURBO": "3.1.0", | ||||
|     "LCMS2": "2.17", | ||||
|     "LIBAVIF": "1.2.1", | ||||
|     "LIBIMAGEQUANT": "4.3.4", | ||||
|     "LIBPNG": "1.6.47", | ||||
|     "LIBWEBP": "1.5.0", | ||||
|  | @ -234,6 +235,7 @@ DEPS: dict[str, dict[str, Any]] = { | |||
|                 "-DBUILD_SHARED_LIBS:BOOL=OFF", | ||||
|                 "-DWebP_LIBRARY=libwebp", | ||||
|                 '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', | ||||
|                 "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", | ||||
|             ) | ||||
|         ], | ||||
|         "headers": [r"libtiff\tiff*.h"], | ||||
|  | @ -378,6 +380,27 @@ DEPS: dict[str, dict[str, Any]] = { | |||
|         ], | ||||
|         "bins": [r"*.dll"], | ||||
|     }, | ||||
|     "libavif": { | ||||
|         "url": f"https://github.com/AOMediaCodec/libavif/archive/v{V['LIBAVIF']}.zip", | ||||
|         "filename": f"libavif-{V['LIBAVIF']}.zip", | ||||
|         "license": "LICENSE", | ||||
|         "build": [ | ||||
|             f"{sys.executable} -m pip install meson", | ||||
|             *cmds_cmake( | ||||
|                 "avif_static", | ||||
|                 "-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", | ||||
|                 "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", | ||||
|             ), | ||||
|             cmd_xcopy("include", "{inc_dir}"), | ||||
|         ], | ||||
|         "libs": ["avif.lib"], | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -683,6 +706,11 @@ def main() -> None: | |||
|         action="store_true", | ||||
|         help="skip LGPL-licensed optional dependency FriBiDi", | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "--no-avif", | ||||
|         action="store_true", | ||||
|         help="skip optional dependency libavif", | ||||
|     ) | ||||
|     args = parser.parse_args() | ||||
| 
 | ||||
|     arch_prefs = ARCHITECTURES[args.architecture] | ||||
|  | @ -723,6 +751,8 @@ def main() -> None: | |||
|         disabled += ["libimagequant"] | ||||
|     if args.no_fribidi: | ||||
|         disabled += ["fribidi"] | ||||
|     if args.no_avif or args.architecture != "AMD64": | ||||
|         disabled += ["libavif"] | ||||
| 
 | ||||
|     prefs = { | ||||
|         "architecture": args.architecture, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user