diff --git a/.ci/install.sh b/.ci/install.sh index cd9035f6a..30b64349d 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -28,7 +28,8 @@ fi python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel -PYTHONOPTIMIZE=0 python3 -m pip install cffi +# TODO Update condition when cffi supports 3.13 +if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi python3 -m pip install coverage python3 -m pip install defusedxml python3 -m pip install olefile @@ -38,7 +39,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy + # TODO Update condition when NumPy supports 3.13 + if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt new file mode 100644 index 000000000..dd61634cd --- /dev/null +++ b/.ci/requirements-cibw.txt @@ -0,0 +1 @@ +cibuildwheel==2.16.2 diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index a20838a15..f41324c4b 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -5,7 +5,9 @@ set -e brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" -PYTHONOPTIMIZE=0 python3 -m pip install cffi +# TODO Update condition when cffi supports 3.13 +if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi + python3 -m pip install coverage python3 -m pip install defusedxml python3 -m pip install olefile @@ -14,7 +16,8 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy +# TODO Update condition when NumPy supports 3.13 +if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index ec22a8184..eb27b4bf7 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -52,6 +52,7 @@ jobs: debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-38-amd64, + fedora-39-amd64, gentoo, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index fd70545ce..8d8cb0b15 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] timeout-minutes: 30 @@ -59,6 +59,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true cache: pip cache-dependency-path: ".github/workflows/test-windows.yml" @@ -71,10 +72,10 @@ jobs: - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH + choco install nasm --no-progress + echo "C:\Program Files\NASM" >> $env:GITHUB_PATH - choco install ghostscript --version=10.0.0.20230317 + choco install ghostscript --version=10.0.0.20230317 --no-progress echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH # Install extra test images @@ -166,7 +167,6 @@ jobs: - name: Build Pillow run: | $FLAGS="-C raqm=vendor -C fribidi=vendor" - if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" } cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -208,47 +208,6 @@ jobs: flags: GHA_Windows name: ${{ runner.os }} Python ${{ matrix.python-version }} - - name: Build wheel - id: wheel - if: "github.event_name != 'pull_request'" - run: | - mkdir fribidi - copy winbuild\build\bin\fribidi* fribidi - setlocal EnableDelayedExpansion - for %%f in (winbuild\build\license\*) do ( - set x=%%~nf - rem Skip FriBiDi license, it is not included in the wheel. - set fribidi=!x:~0,7! - if NOT !fribidi!==fribidi ( - rem Skip imagequant license, it is not included in the wheel. - set libimagequant=!x:~0,13! - if NOT !libimagequant!==libimagequant ( - echo. >> LICENSE - echo ===== %%~nf ===== >> LICENSE - echo. >> LICENSE - type %%f >> LICENSE - ) - ) - ) - for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% - call winbuild\\build\\build_env.cmd - %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable . - shell: cmd - - - name: Upload wheel - uses: actions/upload-artifact@v3 - if: "github.event_name != 'pull_request'" - with: - name: ${{ steps.wheel.outputs.dist }} - path: "*.whl" - - - name: Upload fribidi.dll - if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" - uses: actions/upload-artifact@v3 - with: - name: fribidi - path: fribidi\* - success: permissions: contents: none diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 201f9ef77..33dc561e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,7 @@ jobs: python-version: [ "pypy3.10", "pypy3.9", + "3.13", "3.12", "3.11", "3.10", @@ -64,6 +65,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true cache: pip cache-dependency-path: ".ci/*.sh" diff --git a/.github/workflows/wheels-build.sh b/.github/workflows/wheels-build.sh deleted file mode 100755 index 0aeec6b96..000000000 --- a/.github/workflows/wheels-build.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - # webp, zstd, xz, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb - # libxdmcp causes an issue on macOS < 11 - # curl from brew requires zstd, use system curl - # if php is installed, brew tries to reinstall these after installing openblas - # remove lcms2 and libpng to fix building openjpeg on arm64 - brew remove --ignore-dependencies webp zstd xz libpng libtiff libxcb libxdmcp curl php lcms2 ghostscript - - brew install pkg-config - - if [[ "$PLAT" == "arm64" ]]; then - export MACOSX_DEPLOYMENT_TARGET="11.0" - else - export MACOSX_DEPLOYMENT_TARGET="10.10" - fi -fi - -if [[ "$MB_PYTHON_VERSION" == pypy3* ]]; then - MB_PYTHON_OSX_VER="10.9" -fi - -echo "::group::Install a virtualenv" - source wheels/multibuild/common_utils.sh - source wheels/multibuild/travis_steps.sh - python3 -m pip install virtualenv - before_install -echo "::endgroup::" - -echo "::group::Build wheel" - build_wheel - ls -l "${GITHUB_WORKSPACE}/${WHEEL_SDIR}/" -echo "::endgroup::" - -if [[ $MACOSX_DEPLOYMENT_TARGET != "11.0" ]]; then - echo "::group::Test wheel" - install_run - echo "::endgroup::" -fi diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh new file mode 100755 index 000000000..2605664eb --- /dev/null +++ b/.github/workflows/wheels-dependencies.sh @@ -0,0 +1,151 @@ +#!/bin/bash +# Define custom utilities +# Test for macOS with [ -n "$IS_MACOS" ] +if [ -z "$IS_MACOS" ]; then + export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} + export MB_ML_VER=${AUDITWHEEL_POLICY:9} +fi +export PLAT=$CIBW_ARCHS +source wheels/multibuild/common_utils.sh +source wheels/multibuild/library_builders.sh +if [ -z "$IS_MACOS" ]; then + source wheels/multibuild/manylinux_utils.sh +fi + +ARCHIVE_SDIR=pillow-depends-main + +# Package versions for fresh source builds +FREETYPE_VERSION=2.13.2 +HARFBUZZ_VERSION=8.3.0 +LIBPNG_VERSION=1.6.40 +JPEGTURBO_VERSION=3.0.1 +OPENJPEG_VERSION=2.5.0 +XZ_VERSION=5.4.5 +TIFF_VERSION=4.6.0 +LCMS2_VERSION=2.15 +if [[ -n "$IS_MACOS" ]]; then + GIFLIB_VERSION=5.1.4 +else + GIFLIB_VERSION=5.2.1 +fi +if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then + ZLIB_VERSION=1.3 +else + ZLIB_VERSION=1.2.8 +fi +LIBWEBP_VERSION=1.3.2 +BZIP2_VERSION=1.0.8 +LIBXCB_VERSION=1.16 +BROTLI_VERSION=1.1.0 + +if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then + function build_openjpeg { + local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz) + (cd $out_dir \ + && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && make install) + touch openjpeg-stamp + } +fi + +function build_brotli { + local cmake=$(get_modern_cmake) + local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz) + (cd $out_dir \ + && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ + && make install) + if [[ "$MB_ML_LIBC" == "manylinux" ]]; then + cp /usr/local/lib64/libbrotli* /usr/local/lib + cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig + fi +} + +function build { + if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then + export BUILD_PREFIX="/usr/local" + fi + build_xz + if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then + yum remove -y zlib-devel + fi + build_new_zlib + + build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto + if [ -n "$IS_MACOS" ]; then + if [[ "$CIBW_ARCHS" == "arm64" ]]; then + build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto + build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist + if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then + cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc + fi + fi + else + sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc + fi + build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib + + build_libjpeg_turbo + build_tiff + build_libpng + build_lcms2 + if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then + for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do + cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib + done + fi + build_openjpeg + + ORIGINAL_CFLAGS=$CFLAGS + CFLAGS="$CFLAGS -O3 -DNDEBUG" + if [[ -n "$IS_MACOS" ]]; then + CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" + fi + build_libwebp + CFLAGS=$ORIGINAL_CFLAGS + + build_brotli + + if [ -n "$IS_MACOS" ]; then + # Custom freetype build + build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no + else + build_freetype + fi + + if [ -z "$IS_MACOS" ]; then + export FREETYPE_LIBS=-lfreetype + export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ + fi + build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no + if [ -z "$IS_MACOS" ]; then + export FREETYPE_LIBS="" + export FREETYPE_CFLAGS="" + fi +} + +# Any stuff that you need to do before you start building the wheels +# Runs in the root directory of this repository. +curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +untar pillow-depends-main.zip + +if [[ -n "$IS_MACOS" ]]; then + # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb + # libxdmcp causes an issue on macOS < 11 + # if php is installed, brew tries to reinstall these after installing openblas + # remove cairo to fix building harfbuzz on arm64 + # remove lcms2 and libpng to fix building openjpeg on arm64 + # remove zstd to avoid inclusion on x86_64 + # curl from brew requires zstd, use system curl + brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd + + brew install pkg-config +fi + +wrap_wheel_builder build + +# Append licenses +for filename in wheels/dependency_licenses/*; do + echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE + cat $filename >> LICENSE +done diff --git a/.github/workflows/wheels-linux.yml b/.github/workflows/wheels-linux.yml deleted file mode 100644 index 8b2d9d451..000000000 --- a/.github/workflows/wheels-linux.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Build Linux wheels - -on: - workflow_call: - inputs: - artifacts-name: - required: true - type: string - -env: - CONFIG_PATH: "wheels/config.sh" - REPO_DIR: "." - TEST_DEPENDS: "pytest pytest-timeout" - -jobs: - build: - name: ${{ matrix.python }} ${{ matrix.mb-ml-libc }}${{ matrix.mb-ml-ver }} - runs-on: "ubuntu-latest" - strategy: - fail-fast: false - matrix: - python: [ - "pypy3.9-7.3.13", - "pypy3.10-7.3.13", - "3.8", - "3.9", - "3.10", - "3.11", - "3.12", - ] - mb-ml-libc: [ "manylinux" ] - mb-ml-ver: [ 2014, "_2_28" ] - include: - - python: "3.8" - mb-ml-libc: "musllinux" - mb-ml-ver: "_1_1" - - python: "3.9" - mb-ml-libc: "musllinux" - mb-ml-ver: "_1_1" - - python: "3.10" - mb-ml-libc: "musllinux" - mb-ml-ver: "_1_1" - - python: "3.11" - mb-ml-libc: "musllinux" - mb-ml-ver: "_1_1" - - python: "3.12" - mb-ml-libc: "musllinux" - mb-ml-ver: "_1_1" - env: - MB_PYTHON_VERSION: ${{ matrix.python }} - MB_ML_LIBC: ${{ matrix.mb-ml-libc }} - MB_ML_VER: ${{ matrix.mb-ml-ver }} - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - name: Build Wheel - run: .github/workflows/wheels-build.sh - - uses: actions/upload-artifact@v3 - with: - name: ${{ inputs.artifacts-name }} - path: wheelhouse/*.whl - # Uncomment to get SSH access for testing - # - name: Setup tmate session - # if: failure() - # uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/wheels-macos.yml b/.github/workflows/wheels-macos.yml deleted file mode 100644 index c51abf39a..000000000 --- a/.github/workflows/wheels-macos.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build macOS wheels - -on: - workflow_call: - inputs: - artifacts-name: - required: true - type: string - -env: - CONFIG_PATH: "wheels/config.sh" - REPO_DIR: "." - TEST_DEPENDS: "pytest pytest-timeout" - -jobs: - build: - name: ${{ matrix.python }} ${{ matrix.platform }} - runs-on: "macos-latest" - strategy: - fail-fast: false - matrix: - python: [ - "pypy3.9-7.3.13", - "pypy3.10-7.3.13", - "3.8", - "3.9", - "3.10", - "3.11", - "3.12", - ] - platform: [ "x86_64", "arm64" ] - exclude: - - python: "pypy3.9-7.3.13" - platform: "arm64" - - python: "pypy3.10-7.3.13" - platform: "arm64" - env: - PLAT: ${{ matrix.platform }} - MB_PYTHON_VERSION: ${{ matrix.python }} - TRAVIS_OS_NAME: "osx" - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - uses: actions/setup-python@v4 - with: - python-version: "3.x" - - name: Build Wheel - run: .github/workflows/wheels-build.sh - - uses: actions/upload-artifact@v3 - with: - name: ${{ inputs.artifacts-name }} - path: wheelhouse/*.whl - # Uncomment to get SSH access for testing - # - name: Setup tmate session - # if: failure() - # uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/wheels-test.ps1 b/.github/workflows/wheels-test.ps1 new file mode 100644 index 000000000..f593c7228 --- /dev/null +++ b/.github/workflows/wheels-test.ps1 @@ -0,0 +1,22 @@ +param ([string]$venv, [string]$pillow="C:\pillow") +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +Set-PSDebug -Trace 1 +if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { + # unlike CPython, PyPy requires Visual C++ Redistributable to be installed + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe' + C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null +} +$env:path += ";$pillow\winbuild\build\bin\" +& "$venv\Scripts\activate.ps1" +& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f +cd $pillow +& python -VV +if (!$?) { exit $LASTEXITCODE } +& python selftest.py +if (!$?) { exit $LASTEXITCODE } +& python -m pytest -vx Tests\check_wheel.py +if (!$?) { exit $LASTEXITCODE } +& python -m pytest -vx Tests +if (!$?) { exit $LASTEXITCODE } diff --git a/.github/workflows/wheels-test.sh b/.github/workflows/wheels-test.sh new file mode 100755 index 000000000..207ec1567 --- /dev/null +++ b/.github/workflows/wheels-test.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +if [[ "$OSTYPE" == "darwin"* ]]; then + brew install fribidi + export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" +elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then + apk add curl fribidi +else + yum install -y fribidi +fi +if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then + python3 -m pip install numpy +fi + +if [ ! -d "test-images-main" ]; then + curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip + unzip pillow-test-images.zip + mv test-images-main/* Tests/images +fi + +# Runs tests +python3 selftest.py +python3 -m pytest Tests/check_wheel.py +python3 -m pytest diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 4381a9856..c4737bfc7 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -3,14 +3,20 @@ name: Wheels on: push: paths: - - ".github/workflows/wheels*.yml" + - ".ci/requirements-cibw.txt" + - ".github/workflows/wheel*" - "wheels/*" + - "winbuild/build_prepare.py" + - "winbuild/fribidi.cmake" tags: - "*" pull_request: paths: - - ".github/workflows/wheels*.yml" + - ".ci/requirements-cibw.txt" + - ".github/workflows/wheel*" - "wheels/*" + - "winbuild/build_prepare.py" + - "winbuild/fribidi.cmake" workflow_dispatch: permissions: @@ -20,21 +26,179 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -jobs: - macos: - uses: ./.github/workflows/wheels-macos.yml - with: - artifacts-name: "wheels" +env: + FORCE_COLOR: 1 - linux: - uses: ./.github/workflows/wheels-linux.yml - with: - artifacts-name: "wheels" +jobs: + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - name: "macOS x86_64" + os: macos-latest + archs: x86_64 + macosx_deployment_target: "10.10" + - name: "macOS arm64" + os: macos-latest + archs: arm64 + macosx_deployment_target: "11.0" + - name: "manylinux2014 and musllinux x86_64" + os: ubuntu-latest + archs: x86_64 + - name: "manylinux_2_28 x86_64" + os: ubuntu-latest + archs: x86_64 + build: "*manylinux*" + manylinux: "manylinux_2_28" + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Build wheels + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + python3 -m cibuildwheel --output-dir wheelhouse + env: + CIBW_ARCHS: ${{ matrix.archs }} + CIBW_BUILD: ${{ matrix.build }} + CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} + CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} + CIBW_SKIP: pp38-* + CIBW_TEST_SKIP: "*-macosx_arm64" + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} + + - uses: actions/upload-artifact@v3 + with: + name: dist + path: ./wheelhouse/*.whl + + windows: + name: Windows ${{ matrix.arch }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - arch: x86 + cibw_arch: x86 + - arch: x64 + cibw_arch: AMD64 + - arch: ARM64 + cibw_arch: ARM64 + steps: + - uses: actions/checkout@v4 + + - name: Checkout extra test images + uses: actions/checkout@v4 + with: + repository: python-pillow/test-images + path: Tests\test-images + + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Prepare for build + run: | + choco install nasm --no-progress + echo "C:\Program Files\NASM" >> $env:GITHUB_PATH + + # Install extra test images + xcopy /S /Y Tests\test-images\* Tests\images + + & python.exe -m pip install -r .ci/requirements-cibw.txt + + # Cannot cross-compile FriBiDi (only used for tests) + $FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}") + if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" } + & python.exe winbuild\build_prepare.py -v @FLAGS + shell: pwsh + + - name: Build wheels + run: | + setlocal EnableDelayedExpansion + for %%f in (winbuild\build\license\*) do ( + set x=%%~nf + rem Skip FriBiDi license, it is not included in the wheel. + set fribidi=!x:~0,7! + if NOT !fribidi!==fribidi ( + rem Skip imagequant license, it is not included in the wheel. + set libimagequant=!x:~0,13! + if NOT !libimagequant!==libimagequant ( + echo. >> LICENSE + echo ===== %%~nf ===== >> LICENSE + echo. >> LICENSE + type %%f >> LICENSE + ) + ) + ) + call winbuild\\build\\build_env.cmd + %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse + env: + CIBW_ARCHS: ${{ matrix.cibw_arch }} + CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" + CIBW_CACHE_PATH: "C:\\cibw" + CIBW_TEST_SKIP: "*-win_arm64" + CIBW_TEST_COMMAND: 'docker run --rm + -v {project}:C:\pillow + -v C:\cibw:C:\cibw + -v %CD%\..\venv-test:%CD%\..\venv-test + -e CI -e GITHUB_ACTIONS + mcr.microsoft.com/windows/servercore:ltsc2022 + powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test' + shell: cmd + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: dist + path: ./wheelhouse/*.whl + + - name: Prepare to upload FriBiDi + if: "matrix.arch != 'ARM64'" + run: | + mkdir fribidi\${{ matrix.arch }} + copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }} + shell: cmd + + - name: Upload fribidi.dll + if: "matrix.arch != 'ARM64'" + uses: actions/upload-artifact@v3 + with: + name: fribidi + path: fribidi\* + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: "Makefile" + + - run: make sdist + + - uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/*.tar.gz success: permissions: contents: none - needs: [macos, linux] + needs: [build, windows, sdist] runs-on: ubuntu-latest name: Wheels Successful steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a6b1c6300..8b2dc06ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,6 @@ repos: rev: 23.10.1 hooks: - id: black - args: [--target-version=py38] - repo: https://github.com/PyCQA/bandit rev: 1.7.5 diff --git a/.travis.yml b/.travis.yml index 503f243e6..8f8250809 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,8 @@ if: tag IS present OR type = api env: global: - - CONFIG_PATH=wheels/config.sh - - REPO_DIR=. - - PLAT=aarch64 - - TEST_DEPENDS=pytest-timeout + - CIBW_ARCHS=aarch64 + - CIBW_SKIP=pp38-* language: python # Default Python version is usually 3.6 @@ -15,120 +13,39 @@ services: docker jobs: include: - - name: "3.8 Focal manylinux2014 aarch64" + - name: "manylinux2014 aarch64" os: linux arch: arm64 env: - - MB_ML_VER=2014 - - MB_PYTHON_VERSION=3.8 - - name: "3.8 Focal manylinux_2_28 aarch64" + - CIBW_BUILD="*manylinux*" + - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014 + - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014 + - name: "manylinux_2_28 aarch64" os: linux arch: arm64 env: - - MB_ML_VER="_2_28" - - MB_PYTHON_VERSION=3.8 - - name: "3.8 musllinux_1_1 aarch64" + - CIBW_BUILD="*manylinux*" + - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28 + - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28 + - name: "musllinux aarch64" os: linux arch: arm64 env: - - MB_ML_VER="_1_1" - - MB_ML_LIBC="musllinux" - - MB_PYTHON_VERSION=3.8 - - name: "3.9 Focal manylinux2014 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER=2014 - - MB_PYTHON_VERSION=3.9 - - name: "3.9 Focal manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_2_28" - - MB_PYTHON_VERSION=3.9 - - name: "3.9 musllinux_1_1 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_1_1" - - MB_ML_LIBC="musllinux" - - MB_PYTHON_VERSION=3.9 - - name: "3.10 Focal manylinux2014 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER=2014 - - MB_PYTHON_VERSION=3.10 - - name: "3.10 Focal manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_2_28" - - MB_PYTHON_VERSION=3.10 - - name: "3.10 musllinux_1_1 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_1_1" - - MB_ML_LIBC="musllinux" - - MB_PYTHON_VERSION=3.10 - - name: "3.11 Focal manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER=2014 - - MB_PYTHON_VERSION=3.11 - - name: "3.11 Focal manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_2_28" - - MB_PYTHON_VERSION=3.11 - - name: "3.11 musllinux_1_1 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_1_1" - - MB_ML_LIBC="musllinux" - - MB_PYTHON_VERSION=3.11 - - name: "3.12 Focal manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER=2014 - - MB_PYTHON_VERSION=3.12 - - name: "3.12 Focal manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_2_28" - - MB_PYTHON_VERSION=3.12 - - name: "3.12 musllinux_1_1 aarch64" - os: linux - arch: arm64 - env: - - MB_ML_VER="_1_1" - - MB_ML_LIBC="musllinux" - - MB_PYTHON_VERSION=3.12 - -before_install: - - source wheels/multibuild/common_utils.sh - - source wheels/multibuild/travis_steps.sh - - before_install + - CIBW_BUILD="*musllinux*" install: - - build_multilinux aarch64 build_wheel - - ls -l "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/" + - python3 -m pip install -r .ci/requirements-cibw.txt script: - - install_run + - python3 -m cibuildwheel --output-dir wheelhouse + - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/" # Upload wheels to GitHub Releases deploy: provider: releases api_key: $GITHUB_RELEASE_TOKEN file_glob: true - file: "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/*.whl" + file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl" on: repo: python-pillow/Pillow tags: true diff --git a/CHANGES.rst b/CHANGES.rst index 0f1e419aa..251917654 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,24 @@ Changelog (Pillow) 10.2.0 (unreleased) ------------------- +- Raise ValueError when TrueType font size is not greater than zero #7584 + [akx, radarhere] + +- If absent, do not try to close fp when closing image #7557 + [RaphaelVRossi, radarhere] + +- Allow configuring JPEG restart marker interval on save #7488 + [bgilbert, radarhere] + +- Decrement reference count for PyObject #7549 + [radarhere] + +- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491 + [bgilbert, radarhere] + +- If save_all PNG only has one frame, do not create animated image #7522 + [radarhere] + - Fixed frombytes() for images with a zero dimension #7493 [radarhere] diff --git a/Makefile b/Makefile index b7f07e24d..ad0a1adab 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,6 @@ lint: .PHONY: lint-fix lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black - python3 -m black --target-version py38 . + python3 -m black . python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff python3 -m ruff --fix . diff --git a/RELEASING.md b/RELEASING.md index 02551a3a9..8b0673203 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -20,12 +20,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. git tag 5.2.0 git push --tags ``` -* [ ] Create and check source distribution: - ```bash - make sdist - ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) -* [ ] Check and upload all binaries and source distributions e.g.: +* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) +* [ ] Check and upload all source and binary distributions e.g.: ```bash python3 -m twine check --strict dist/* python3 -m twine upload dist/Pillow-5.2.0* @@ -59,8 +55,8 @@ Released as needed for security, installation or critical bug fixes. ```bash make sdist ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) -* [ ] Check and upload all binaries and source distributions e.g.: +* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) +* [ ] Check and upload all source and binary distributions e.g.: ```bash python3 -m twine check --strict dist/* python3 -m twine upload dist/Pillow-5.2.1* @@ -90,20 +86,20 @@ Released as needed privately to individual vendors for critical security-related ```bash make sdist ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) +* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: ```bash git push origin 2.5.x ``` -## Binary Distributions +## Source and Binary Distributions ### macOS and Linux -* [ ] Download wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) +* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): ```bash gh run download --dir dist - # select wheels + # select dist ``` * [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases) and copy into `dist`. diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index d94b1985b..36ce63296 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -45,7 +45,7 @@ def test_direct(): assert caccess[(0, 0)] == access[(0, 0)] - print("Size: %sx%s" % im.size) # noqa: UP031 + print(f"Size: {im.width}x{im.height}") timer(iterate_get, "PyAccess - get", im.size, access) timer(iterate_set, "PyAccess - set", im.size, access) timer(iterate_get, "C-api - get", im.size, caccess) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py new file mode 100644 index 000000000..cc52cb75e --- /dev/null +++ b/Tests/check_wheel.py @@ -0,0 +1,41 @@ +import sys + +from PIL import features + + +def test_wheel_modules(): + expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} + + # tkinter is not available in cibuildwheel installed CPython on Windows + try: + import tkinter + + assert tkinter + except ImportError: + expected_modules.remove("tkinter") + + assert set(features.get_supported_modules()) == expected_modules + + +def test_wheel_codecs(): + expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} + + assert set(features.get_supported_codecs()) == expected_codecs + + +def test_wheel_features(): + expected_features = { + "webp_anim", + "webp_mux", + "transp_webp", + "raqm", + "fribidi", + "harfbuzz", + "libjpeg_turbo", + "xcb", + } + + if sys.platform == "win32": + expected_features.remove("xcb") + + assert set(features.get_supported_features()) == expected_features diff --git a/Tests/helper.py b/Tests/helper.py index de5468d84..cce7eca3a 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -5,6 +5,7 @@ Helper functions. import logging import os import shutil +import subprocess import sys import sysconfig import tempfile @@ -95,7 +96,7 @@ def assert_image_equal(a, b, msg=None): except Exception: pass - assert False, msg or "got different content" + pytest.fail(msg or "got different content") def assert_image_equal_tofile(a, filename, msg=None, mode=None): @@ -258,11 +259,21 @@ def hopper(mode=None, cache={}): def djpeg_available(): - return bool(shutil.which("djpeg")) + if shutil.which("djpeg"): + try: + subprocess.check_call(["djpeg", "-version"]) + return True + except subprocess.CalledProcessError: # pragma: no cover + return False def cjpeg_available(): - return bool(shutil.which("cjpeg")) + if shutil.which("cjpeg"): + try: + subprocess.check_call(["cjpeg", "-version"]) + return True + except subprocess.CalledProcessError: # pragma: no cover + return False def netpbm_available(): diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index d2edcfc27..dac35a8d0 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,6 +1,8 @@ import sys from io import BytesIO, StringIO +import pytest + from PIL import Image, IptcImagePlugin from .helper import hopper @@ -44,7 +46,7 @@ def test_getiptcinfo_fotostation(): for tag in iptc.keys(): if tag[0] == 240: return - assert False, "FotoStation tag not found" + pytest.fail("FotoStation tag not found") def test_getiptcinfo_zero_padding(): diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index a0822d000..ef070b6c5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -643,6 +643,23 @@ class TestFileJpeg: assert max(im2.quantization[0]) <= 255 assert max(im2.quantization[1]) <= 255 + @pytest.mark.parametrize( + "blocks, rows, markers", + ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), + ) + def test_restart_markers(self, blocks, rows, markers): + im = Image.new("RGB", (32, 32)) # 16 MCUs + out = BytesIO() + im.save( + out, + format="JPEG", + restart_marker_blocks=blocks, + restart_marker_rows=rows, + # force 8x8 pixel MCUs + subsampling=0, + ) + assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: @@ -961,6 +978,28 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False + def test_separate_tables(self): + im = hopper() + data = [] # [interchange, tables-only, image-only] + for streamtype in range(3): + out = BytesIO() + im.save(out, format="JPEG", streamtype=streamtype) + data.append(out.getvalue()) + + # SOI, EOI + for marker in b"\xff\xd8", b"\xff\xd9": + assert marker in data[1] and marker in data[2] + # DHT, DQT + for marker in b"\xff\xc4", b"\xff\xdb": + assert marker in data[1] and marker not in data[2] + # SOF0, SOS, APP0 (JFIF header) + for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": + assert marker not in data[1] and marker in data[2] + + with Image.open(BytesIO(data[0])) as interchange_im: + with Image.open(BytesIO(data[1] + data[2])) as combined_im: + assert_image_equal(interchange_im, combined_im) + def test_repr_jpeg(self): im = hopper() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 99df26fc9..2016b3ccb 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -416,7 +416,7 @@ def test_plt_marker(): while True: marker = out.read(2) if not marker: - assert False, "End of stream without PLT" + pytest.fail("End of stream without PLT") jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: @@ -426,7 +426,7 @@ def test_plt_marker(): # PLT return elif jp2_boxid == 0xFF93: - assert False, "SOD without finding PLT first" + pytest.fail("SOD without finding PLT first") hdr = out.read(2) length = _binary.i16be(hdr) diff --git a/Tests/test_image.py b/Tests/test_image.py index 039eb33d1..f0861bb4f 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,4 +1,5 @@ import io +import logging import os import shutil import sys @@ -999,7 +1000,7 @@ class TestImage: with Image.open(os.path.join("Tests/images", path)) as im: try: im.load() - assert False + pytest.fail() except OSError as e: buffer_overrun = str(e) == "buffer overrun when reading image file" truncated = "image file is truncated" in str(e) @@ -1010,10 +1011,19 @@ class TestImage: with Image.open("Tests/images/fli_overrun2.bin") as im: try: im.seek(1) - assert False + pytest.fail() except OSError as e: assert str(e) == "buffer overrun when reading image file" + def test_close_graceful(self, caplog): + with Image.open("Tests/images/hopper.jpg") as im: + copy = im.copy() + with caplog.at_level(logging.DEBUG): + im.close() + copy.close() + assert len(caplog.records) == 0 + assert im.fp is None + class MockEncoder: pass diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 981753eb9..3bafc4c9c 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -67,7 +67,7 @@ def test_quantize_no_dither(): def test_quantize_no_dither2(): im = Image.new("RGB", (9, 1)) - im.putdata(list((p,) * 3 for p in range(0, 36, 4))) + im.putdata([(p,) * 3 for p in range(0, 36, 4)]) palette = Image.new("P", (1, 1)) data = (0, 0, 0, 32, 32, 32) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 83c54cf62..b5bfa903f 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -195,7 +195,7 @@ class TestReducingGapResize: (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 ) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): assert_image_equal(ref, im) assert_image_similar(ref, im, epsilon) @@ -210,7 +210,7 @@ class TestReducingGapResize: (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 ) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): assert_image_equal(ref, im) assert_image_similar(ref, im, epsilon) @@ -225,7 +225,7 @@ class TestReducingGapResize: (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 ) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): assert_image_equal(ref, im) assert_image_similar(ref, im, epsilon) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 4fd07a2b4..96a2c2662 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -147,7 +147,7 @@ def test_reducing_gap_values(): ref = hopper() ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): assert_image_equal(ref, im) assert_image_similar(ref, im, 3.5) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index fd5531cd8..f19fb7690 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1082,3 +1082,9 @@ def test_raqm_missing_warning(monkeypatch): "Raqm layout was requested, but Raqm is not available. " "Falling back to basic layout." ) + + +@pytest.mark.parametrize("size", [-1, 0]) +def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size): + with pytest.raises(ValueError): + ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index f8059eca4..a75cbadc4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -11,6 +11,10 @@ from .helper import assert_image_equal_tofile, skip_unless_feature class TestImageGrab: + @pytest.mark.skipif( + os.environ.get("USERNAME") == "ContainerAdministrator", + reason="can't grab screen when running in Docker", + ) @pytest.mark.skipif( sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index e54372b60..3e73339ed 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -85,7 +85,7 @@ def test_ipythonviewer(): test_viewer = viewer break else: - assert False + pytest.fail() im = hopper() assert test_viewer.show(im) == 1 diff --git a/docs/conf.py b/docs/conf.py index ef2cb5b88..833dfa215 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -166,6 +166,12 @@ html_static_path = ["resources"] # directly to the root of the documentation. # html_extra_path = [] +html_css_files = ["css/dark.css"] + +html_js_files = [ + "js/activate_tab.js", +] + # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' @@ -313,10 +319,6 @@ texinfo_documents = [ # texinfo_no_detailmenu = False -def setup(app): - app.add_css_file("css/dark.css") - - linkcheck_allowed_redirects = { r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 3cf5ad765..38c00f870 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -266,9 +266,12 @@ following options are available:: :py:class:`PIL.ImagePalette.ImagePalette` object. **optimize** - If present and true, attempt to compress the palette by - eliminating unused colors. This is only useful if the palette can - be compressed to the next smaller power of 2 elements. + Whether to attempt to compress the palette by eliminating unused colors. + This is attempted by default, unless a palette is specified as an option or + as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary. + + This is only useful if the palette can be compressed to the next smaller + power of 2 elements. Note that if the image you are saving comes from an existing GIF, it may have the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary. @@ -494,6 +497,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If absent, the setting will be determined by libjpeg or libjpeg-turbo. +**restart_marker_blocks** + If present, emit a restart marker whenever the specified number of MCU + blocks has been produced. + + .. versionadded:: 10.2.0 + +**restart_marker_rows** + If present, emit a restart marker whenever the specified number of MCU + rows has been produced. + + .. versionadded:: 10.2.0 + **qtables** If present, sets the qtables for the encoder. This is listed as an advanced option for wizards in the JPEG documentation. Use with diff --git a/docs/installation.rst b/docs/installation.rst index ab15fe643..78900aa57 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,6 +1,14 @@ Installation ============ +.. raw:: html + + + Warnings -------- @@ -151,13 +159,13 @@ Many of Pillow's features require external libraries: * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and libjpeg-turbo version **8**. - * Starting with Pillow 3.0.0, libjpeg is required by default, but - may be disabled with the ``--disable-jpeg`` flag. + * Starting with Pillow 3.0.0, libjpeg is required by default. It can be + disabled with the ``-C jpeg=disable`` flag. * **zlib** provides access to compressed PNGs - * Starting with Pillow 3.0.0, zlib is required by default, but may - be disabled with the ``--disable-zlib`` flag. + * Starting with Pillow 3.0.0, zlib is required by default. It can be + disabled with the ``-C zlib=disable`` flag. * **libtiff** provides compressed TIFF functionality @@ -176,8 +184,6 @@ Many of Pillow's features require external libraries: transparent WebP files. Versions **0.3.0** and above support transparency. -* **tcl/tk** provides support for tkinter bitmap and photo images. - * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, @@ -463,6 +469,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 38 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 39 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | @@ -574,6 +582,10 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+----------------------------+------------------+--------------+ | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ +| Windows 11 | 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm64 | ++----------------------------------+----------------------------+------------------+--------------+ +| Windows 11 Pro | 3.11, 3.12 | 10.1.0 |x86-64 | ++----------------------------------+----------------------------+------------------+--------------+ | Windows 10 | 3.7 | 7.1.0 |x86-64 | +----------------------------------+----------------------------+------------------+--------------+ | Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | diff --git a/docs/resources/js/activate_tab.js b/docs/resources/js/activate_tab.js new file mode 100644 index 000000000..92522b5ce --- /dev/null +++ b/docs/resources/js/activate_tab.js @@ -0,0 +1,36 @@ +// Based on https://stackoverflow.com/a/38241481/724176 +function getOS() { + const userAgent = window.navigator.userAgent, + platform = window.navigator.userAgentData?.platform || window.navigator.platform, + macosPlatforms = ["macOS", "Macintosh", "MacIntel", "MacPPC", "Mac68K"], + windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"]; + + if (macosPlatforms.includes(platform)) { + return "macOS"; + } else if (windowsPlatforms.includes(platform)) { + return "Windows"; + } else if (/Android/.test(userAgent)) { + return "Android"; + } else if (/Linux/.test(platform)) { + return "Linux"; + } +} + +function activateTab(tabName) { + // Find all label elements with the specified tab name + const labels = document.querySelectorAll(".tab-label"); + + labels.forEach((label) => { + if (label.textContent == tabName) { + // Find the associated input element using the "for" attribute + const tabInputId = label.getAttribute("for"); + const tabInput = document.getElementById(tabInputId); + + // Check if the input element exists before attempting to set the "checked" attribute + if (tabInput) { + // Activate the tab by setting its "checked" attribute to true + tabInput.checked = true; + } + } + }); +} diff --git a/pyproject.toml b/pyproject.toml index 59d8da44e..f9cea0612 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,12 @@ docs = [ "sphinx-removed-in", "sphinxext-opengraph", ] +fpx = [ + "olefile", +] +mic = [ + "olefile", +] tests = [ "check-manifest", "coverage", @@ -59,6 +65,9 @@ tests = [ "pytest-cov", "pytest-timeout", ] +xmp = [ + "defusedxml", +] [project.urls] Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" Documentation = "https://pillow.readthedocs.io" @@ -77,10 +86,17 @@ package-dir = {"" = "src"} [tool.setuptools.dynamic] version = {attr = "PIL.__version__"} +[tool.cibuildwheel] +before-all = ".github/workflows/wheels-dependencies.sh" +build-verbosity = 1 +config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" +test-command = "cd {project} && .github/workflows/wheels-test.sh" +test-extras = "tests" + [tool.ruff] -target-version = "py38" line-length = 88 select = [ + "C4", # flake8-comprehensions "E", # pycodestyle errors "EM", # flake8-errmsg "F", # pyflakes errors diff --git a/setup.py b/setup.py index f13f03713..2a364ba97 100755 --- a/setup.py +++ b/setup.py @@ -440,17 +440,17 @@ class pil_build_ext(build_ext): # # add configured kits - for root_name, lib_name in dict( - JPEG_ROOT="libjpeg", - JPEG2K_ROOT="libopenjp2", - TIFF_ROOT=("libtiff-5", "libtiff-4"), - ZLIB_ROOT="zlib", - FREETYPE_ROOT="freetype2", - HARFBUZZ_ROOT="harfbuzz", - FRIBIDI_ROOT="fribidi", - LCMS_ROOT="lcms2", - IMAGEQUANT_ROOT="libimagequant", - ).items(): + for root_name, lib_name in { + "JPEG_ROOT": "libjpeg", + "JPEG2K_ROOT": "libopenjp2", + "TIFF_ROOT": ("libtiff-5", "libtiff-4"), + "ZLIB_ROOT": "zlib", + "FREETYPE_ROOT": "freetype2", + "HARFBUZZ_ROOT": "harfbuzz", + "FRIBIDI_ROOT": "fribidi", + "LCMS_ROOT": "lcms2", + "IMAGEQUANT_ROOT": "libimagequant", + }.items(): root = globals()[root_name] if root is None and root_name in os.environ: diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index ef719e3ec..b51019c66 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -396,7 +396,7 @@ def _save(im, fp, filename, bitmap_header=True): dpi = info.get("dpi", (96, 96)) # 1 meter == 39.3701 inches - ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi)) + ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi) stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3) header = 40 # or 64 for OS/2 version 2 diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index 94efff341..fc0dae44b 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -64,8 +64,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): d, e, o, a = self.tile[0] self.tile[0] = d, (0, 0) + self.size, o, a - return - # # -------------------------------------------------------------------- diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 63369eb64..c05208c80 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -77,14 +77,11 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): # Hack to support hi-res rendering scale = int(scale) or 1 - # orig_size = size - # orig_bbox = bbox - size = (size[0] * scale, size[1] * scale) + width = size[0] * scale + height = size[1] * scale # resolution is dependent on bbox and size - res = ( - 72.0 * size[0] / (bbox[2] - bbox[0]), - 72.0 * size[1] / (bbox[3] - bbox[1]), - ) + res_x = 72.0 * width / (bbox[2] - bbox[0]) + res_y = 72.0 * height / (bbox[3] - bbox[1]) out_fd, outfile = tempfile.mkstemp() os.close(out_fd) @@ -121,8 +118,8 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): command = [ gs_binary, "-q", # quiet mode - "-g%dx%d" % size, # set output geometry (pixels) - "-r%fx%f" % res, # set input DPI (dots per inch) # noqa: UP031 + f"-g{width:d}x{height:d}", # set output geometry (pixels) + f"-r{res_x:f}x{res_y:f}", # set input DPI (dots per inch) "-dBATCH", # exit after processing "-dNOPAUSE", # don't pause between pages "-dSAFER", # safe mode diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index e0e51aaac..1ff8a7e91 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -54,12 +54,10 @@ class FitsImageFile(ImageFile.ImageFile): self._mode = "L" elif number_of_bits == 16: self._mode = "I" - # rawmode = "I;16S" elif number_of_bits == 32: self._mode = "I" elif number_of_bits in (-32, -64): self._mode = "F" - # rawmode = "F" if number_of_bits == -32 else "F;64F" offset = math.ceil(self.fp.tell() / 2880) * 2880 self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))] diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 5ec0a6632..085917ac3 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -78,7 +78,6 @@ class FontFile: if glyph: d, dst, src, im = glyph xx = src[2] - src[0] - # yy = src[3] - src[1] x0, y0 = x, y x = x + xx if x > WIDTH: diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index a878cbfd2..3027ef45b 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -227,6 +227,7 @@ class FpxImageFile(ImageFile.ImageFile): break # isn't really required self.stream = stream + self._fp = self.fp self.fp = None def load(self): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4ce295f7f..0793d4b42 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -281,14 +281,9 @@ class GifImageFile(ImageFile.ImageFile): bits = self.fp.read(1)[0] self.__offset = self.fp.tell() break - - else: - pass - # raise OSError, "illegal GIF tag `%x`" % s[0] s = None if interlace is None: - # self._fp = None msg = "image not found in GIF frame" raise EOFError(msg) @@ -661,7 +656,7 @@ def _save(im, fp, filename, save_all=False): palette = im.encoderinfo.get("palette", im.info.get("palette")) else: palette = None - im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) + im.encoderinfo.setdefault("optimize", True) if not save_all or not _write_multiple_frames(im, fp, palette): _write_single_frame(im, fp, palette) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 5226c986d..b415a3219 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -391,8 +391,8 @@ if __name__ == "__main__": with open(sys.argv[1], "rb") as fp: imf = IcnsImageFile(fp) for size in imf.info["sizes"]: - imf.size = size - imf.save("out-%s-%s-%s.png" % size) # noqa: UP031 + width, height, scale = imf.size = size + imf.save(f"out-{width}-{height}-{scale}.png") with Image.open(sys.argv[1]) as im: im.save("out.png") if sys.platform == "windows": diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 0445a2ab2..7f0f0047c 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -174,9 +174,7 @@ class IcoFile: self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) # ICO images are usually squares - # self.entry = sorted(self.entry, key=lambda x: x['width']) - self.entry = sorted(self.entry, key=lambda x: x["square"]) - self.entry.reverse() + self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True) def sizes(self): """ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 930ca060b..2853bd596 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -40,7 +40,7 @@ from enum import IntEnum from pathlib import Path try: - import defusedxml.ElementTree as ElementTree + from defusedxml import ElementTree except ImportError: ElementTree = None @@ -549,16 +549,17 @@ class Image: :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for more information. """ - try: - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - self.fp = None - except Exception as msg: - logger.debug("Error closing: %s", msg) + if hasattr(self, "fp"): + try: + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + self.fp = None + except Exception as msg: + logger.debug("Error closing: %s", msg) if getattr(self, "map", None): self.map = None @@ -1159,7 +1160,7 @@ class Image: if palette.mode != "P": msg = "bad mode for palette image" raise ValueError(msg) - if self.mode != "RGB" and self.mode != "L": + if self.mode not in {"RGB", "L"}: msg = "only RGB or L mode images can be quantized to a palette" raise ValueError(msg) im = self.im.convert("P", dither, palette.im) @@ -3100,7 +3101,8 @@ def fromarray(obj, mode=None): try: mode, rawmode = _fromarray_typemap[typekey] except KeyError as e: - msg = "Cannot handle this data type: %s, %s" % typekey # noqa: UP031 + typekey_shape, typestr = typekey + msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" raise TypeError(msg) from e else: rawmode = mode diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 40eed46bc..098a317fe 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -923,7 +923,7 @@ def floodfill(image, xy, value, border=None, thresh=0): if border is None: fill = _color_diff(p, background) <= thresh else: - fill = p != value and p != border + fill = p not in (value, border) if fill: pixel[s, t] = value new_edge.add((s, t)) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 8432a187f..902e8ce5f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -430,7 +430,6 @@ class Parser: with io.BytesIO(self.data) as fp: im = Image.open(fp) except OSError: - # traceback.print_exc() pass # not enough data else: flag = hasattr(im, "load_seek") or hasattr(im, "load_read") diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c29562135..0331a5c45 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -788,8 +788,13 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): .. versionadded:: 4.2.0 :return: A font object. :exception OSError: If the file could not be read. + :exception ValueError: If the font size is not greater than zero. """ + if size <= 0: + msg = "font size must be greater than 0" + raise ValueError(msg) + def freetype(font): return FreeTypeFont(font, size, index, encoding, layout_engine) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 1ba50b5ec..cb4f1dba1 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -257,8 +257,6 @@ def load(filename): if lut: break except (SyntaxError, ValueError): - # import traceback - # traceback.print_exc() pass else: msg = "cannot load palette" diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 9b7245454..d017565a9 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -83,16 +83,6 @@ def fromqimage(im): def fromqpixmap(im): return fromqimage(im) - # buffer = QBuffer() - # buffer.open(QIODevice.ReadWrite) - # # im.save(buffer) - # # What if png doesn't support some image features like animation? - # im.save(buffer, 'ppm') - # bytes_io = BytesIO() - # bytes_io.write(buffer.data()) - # buffer.close() - # bytes_io.seek(0) - # return Image.open(bytes_io) def align8to32(bytes, width, mode): @@ -208,9 +198,5 @@ def toqimage(im): def toqpixmap(im): - # # This doesn't work. For now using a dumb approach. - # im_data = _toqclass_helper(im) - # result = QPixmap(im_data["size"][0], im_data["size"][1]) - # result.loadFromData(im_data["data"]) qimage = toqimage(im) return QPixmap.fromImage(qimage) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 963d6c1a3..bb0cb676a 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -334,10 +334,7 @@ def _save(im, fp, filename): if quality_layers is not None and not ( isinstance(quality_layers, (list, tuple)) and all( - [ - isinstance(quality_layer, (int, float)) - for quality_layer in quality_layers - ] + isinstance(quality_layer, (int, float)) for quality_layer in quality_layers ) ): msg = "quality_layers must be a sequence of numbers" diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index c091697f5..b8a5e7a59 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -397,7 +397,7 @@ class JpegImageFile(ImageFile.ImageFile): # self.__offset = self.fp.tell() break s = self.fp.read(1) - elif i == 0 or i == 0xFFFF: + elif i in {0, 0xFFFF}: # padded marker or junk; move on s = b"\xff" elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) @@ -787,6 +787,8 @@ def _save(im, fp, filename): dpi[0], dpi[1], subsampling, + info.get("restart_marker_blocks", 0), + info.get("restart_marker_rows", 0), qtables, comment, extra, diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 801318930..e4154902f 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -66,6 +66,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 + self.__fp = self.fp self.seek(0) def seek(self, frame): @@ -87,10 +88,12 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): return self.frame def close(self): + self.__fp.close() self.ole.close() super().close() def __exit__(self, *args): + self.__fp.close() self.ole.close() super().__exit__() diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index f9261c77d..89083b4ff 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -33,9 +33,6 @@ from . import ( from ._binary import i16be as i16 from ._binary import o32le -# def _accept(prefix): -# return JpegImagePlugin._accept(prefix) - def _save(im, fp, filename): JpegImagePlugin._save(im, fp, filename) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 07df577ae..8bdb65cce 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -82,7 +82,7 @@ class IndirectReference( collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"]) ): def __str__(self): - return "%s %s R" % self # noqa: UP031 + return f"{self.object_id} {self.generation} R" def __bytes__(self): return self.__str__().encode("us-ascii") @@ -103,7 +103,7 @@ class IndirectReference( class IndirectObjectDef(IndirectReference): def __str__(self): - return "%s %s obj" % self # noqa: UP031 + return f"{self.object_id} {self.generation} obj" class XrefTable: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index e480ab055..93f1528c5 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -328,9 +328,6 @@ def _save(im, fp, filename): fp.write(b"65535\n") ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) - # ALTERNATIVE: save via builtin debug function - # im._dump(filename) - # # -------------------------------------------------------------------- diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index acb9ce5a3..a2a259c89 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -123,7 +123,7 @@ class SgiImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": + if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -155,7 +155,7 @@ def _save(im, fp, filename): # Z Dimension: Number of channels z = len(im.mode) - if dim == 1 or dim == 2: + if dim in {1, 2}: z = 1 # assert we've got the right number of bands. diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index dabf8dbfb..a78a5aef1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1885,13 +1885,14 @@ class AppendingTiffWriter: 8, # long8 ] - # StripOffsets = 273 - # FreeOffsets = 288 - # TileOffsets = 324 - # JPEGQTables = 519 - # JPEGDCTables = 520 - # JPEGACTables = 521 - Tags = {273, 288, 324, 519, 520, 521} + Tags = { + 273, # StripOffsets + 288, # FreeOffsets + 324, # TileOffsets + 519, # JPEGQTables + 520, # JPEGDCTables + 521, # JPEGACTables + } def __init__(self, fn, new=False): if hasattr(fn, "read"): @@ -1941,8 +1942,6 @@ class AppendingTiffWriter: iimm = self.f.read(4) if not iimm: - # msg = "nothing written into new page" - # raise RuntimeError(msg) # Make it easy to finish a frame without committing to a new one. return diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 30b05e4e1..b02c637b6 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -56,7 +56,7 @@ def lookup(tag, group=None): ## # Map tag numbers to tag info. # -# id: (Name, Type, Length, enum_values) +# id: (Name, Type, Length[, enum_values]) # # The length here differs from the length in the tiff spec. For # numbers, the tiff spec is for the number of fields returned. We @@ -427,7 +427,7 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) - for group, tags in TAGS_V2_GROUPS.items(): + for tags in TAGS_V2_GROUPS.values(): for k, v in tags.items(): tags[k] = TagInfo(k, *v) @@ -438,22 +438,6 @@ _populate() TYPES = {} -# was: -# TYPES = { -# 1: "byte", -# 2: "ascii", -# 3: "short", -# 4: "long", -# 5: "rational", -# 6: "signed byte", -# 7: "undefined", -# 8: "signed short", -# 9: "signed long", -# 10: "signed rational", -# 11: "float", -# 12: "double", -# } - # # These tags are handled by default in libtiff, without # adding to the custom dictionary. From tif_dir.c, searching for diff --git a/src/_imagingft.c b/src/_imagingft.c index e2a7927b7..71fac0264 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -885,7 +885,9 @@ font_render(FontObject *self, PyObject *args) { PyMem_Del(glyph_info); return NULL; } - id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); + PyObject *imageId = PyObject_GetAttrString(image, "id"); + id = PyLong_AsSsize_t(imageId); + Py_XDECREF(imageId); im = (Imaging)id; x_offset -= stroke_width; diff --git a/src/encode.c b/src/encode.c index 08544aede..4664ad0f3 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1045,6 +1045,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ Py_ssize_t xdpi = 0, ydpi = 0; Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ + Py_ssize_t restart_marker_blocks = 0; + Py_ssize_t restart_marker_rows = 0; PyObject *qtables = NULL; unsigned int *qarrays = NULL; int qtablesLen = 0; @@ -1057,7 +1059,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnOz#y#y#", + "ss|nnnnnnnnnnOz#y#y#", &mode, &rawmode, &quality, @@ -1068,6 +1070,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &xdpi, &ydpi, &subsampling, + &restart_marker_blocks, + &restart_marker_rows, &qtables, &comment, &comment_size, @@ -1156,6 +1160,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; + ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_blocks = restart_marker_blocks; + ((JPEGENCODERSTATE *)encoder->state.context)->restart_marker_rows = restart_marker_rows; ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 1d7550818..5cc74e69b 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -83,6 +83,10 @@ typedef struct { /* Chroma Subsampling (-1=default, 0=none, 1=medium, 2=high) */ int subsampling; + /* Restart marker interval, in MCU blocks or MCU rows, or 0 for none */ + unsigned int restart_marker_blocks; + unsigned int restart_marker_rows; + /* Converter input mode (input to the shuffler) */ char rawmode[8 + 1]; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 2a24eff39..9da830b18 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -210,6 +210,8 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } context->cinfo.smoothing_factor = context->smooth; context->cinfo.optimize_coding = (boolean)context->optimize; + context->cinfo.restart_interval = context->restart_marker_blocks; + context->cinfo.restart_in_rows = context->restart_marker_rows; if (context->xdpi > 0 && context->ydpi > 0) { context->cinfo.write_JFIF_header = TRUE; context->cinfo.density_unit = 1; /* dots per inch */ @@ -218,9 +220,9 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } switch (context->streamtype) { case 1: - /* tables only -- not yet implemented */ - state->errcode = IMAGING_CODEC_CONFIG; - return -1; + /* tables only */ + jpeg_write_tables(&context->cinfo); + goto cleanup; case 2: /* image only */ jpeg_suppress_tables(&context->cinfo, TRUE); @@ -316,6 +318,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } jpeg_finish_compress(&context->cinfo); +cleanup: /* Clean up */ if (context->comment) { free(context->comment); diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 35122f182..e3b81590e 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -14,6 +14,10 @@ #ifdef HAVE_LIBTIFF +#ifdef HAVE_UNISTD_H +#include /* lseek */ +#endif + #ifndef uint #define uint uint32 #endif diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index c7c7d48ed..02454ba03 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -13,12 +13,6 @@ #include #endif -/* UNDONE -- what are we using from this? */ -/*#ifndef _UNISTD_H - # include - # endif -*/ - #ifndef min #define min(x, y) ((x > y) ? y : x) #define max(x, y) ((x < y) ? y : x) diff --git a/wheels/README.md b/wheels/README.md index c15c034b6..8b412b7fe 100644 --- a/wheels/README.md +++ b/wheels/README.md @@ -1,7 +1,11 @@ README ------ -This directory creates wheels for tagged versions of Pillow. +[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux +wheels for tagged versions of Pillow. + +This directory contains [multibuild](https://github.com/multi-build/multibuild) to +build dependencies for the wheels, and dependency licenses to be included. Archives -------- @@ -16,8 +20,8 @@ But, the build will look in that repository before downloading from the URL, so if there is a library that often fails to download, or you think might fail to download, then download it and add it to the Git repository. -See the `pre_build` in `config.sh` and the `fetch_unpack` routine in -`multibuild/common_utils.sh` for the logic, and the build recipes in +See `build` in `.github/workflows/wheels-dependencies.sh` and the `fetch_unpack` +routine in `multibuild/common_utils.sh` for the logic, and the build recipes in `multibuild/library_builders.sh` for the filename to give to the downloaded archive. @@ -27,5 +31,5 @@ Wheels Wheels are [GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml). -Windows wheels are not created here. Instead, they are +Windows wheels are created separately. They are [GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain). diff --git a/wheels/config.sh b/wheels/config.sh deleted file mode 100644 index f36b98f25..000000000 --- a/wheels/config.sh +++ /dev/null @@ -1,187 +0,0 @@ -# Define custom utilities -# Test for macOS with [ -n "$IS_MACOS" ] - -ARCHIVE_SDIR=pillow-depends-main - -# Package versions for fresh source builds -FREETYPE_VERSION=2.13.2 -HARFBUZZ_VERSION=8.2.1 -LIBPNG_VERSION=1.6.40 -JPEGTURBO_VERSION=3.0.1 -OPENJPEG_VERSION=2.5.0 -XZ_VERSION=5.4.4 -TIFF_VERSION=4.6.0 -LCMS2_VERSION=2.15 -if [[ -n "$IS_MACOS" ]]; then - GIFLIB_VERSION=5.1.4 -else - GIFLIB_VERSION=5.2.1 -fi -if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then - ZLIB_VERSION=1.3 -else - ZLIB_VERSION=1.2.8 -fi -LIBWEBP_VERSION=1.3.2 -BZIP2_VERSION=1.0.8 -LIBXCB_VERSION=1.16 -BROTLI_VERSION=1.1.0 - -if [[ -n "$IS_MACOS" ]] && [[ "$PLAT" == "x86_64" ]]; then - function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz) - (cd $out_dir \ - && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ - && make install) - touch openjpeg-stamp - } -fi - -function build_brotli { - local cmake=$(get_modern_cmake) - local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz) - (cd $out_dir \ - && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ - && make install) - if [[ "$MB_ML_LIBC" == "manylinux" ]]; then - cp /usr/local/lib64/libbrotli* /usr/local/lib - cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig - fi -} - -function pre_build { - # Any stuff that you need to do before you start building the wheels - # Runs in the root directory of this repository. - curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip - untar pillow-depends-main.zip - - build_xz - if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then - yum remove -y zlib-devel - fi - build_new_zlib - - if [ -n "$IS_MACOS" ]; then - ORIGINAL_BUILD_PREFIX=$BUILD_PREFIX - ORIGINAL_PKG_CONFIG_PATH=$PKG_CONFIG_PATH - BUILD_PREFIX=`dirname $(dirname $(which python))` - PKG_CONFIG_PATH="$BUILD_PREFIX/lib/pkgconfig" - fi - build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto - if [ -n "$IS_MACOS" ]; then - build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - cp venv/share/pkgconfig/xcb-proto.pc venv/lib/pkgconfig/xcb-proto.pc - else - sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc - fi - build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib - if [ -n "$IS_MACOS" ]; then - BUILD_PREFIX=$ORIGINAL_BUILD_PREFIX - PKG_CONFIG_PATH=$ORIGINAL_PKG_CONFIG_PATH - fi - - build_libjpeg_turbo - if [[ -n "$IS_MACOS" ]]; then - rm /usr/local/lib/libjpeg.dylib - fi - build_tiff - build_libpng - build_lcms2 - build_openjpeg - - ORIGINAL_CFLAGS=$CFLAGS - CFLAGS="$CFLAGS -O3 -DNDEBUG" - if [[ -n "$IS_MACOS" ]]; then - CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names" - fi - build_libwebp - CFLAGS=$ORIGINAL_CFLAGS - - build_brotli - - if [ -n "$IS_MACOS" ]; then - # Custom freetype build - build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no - else - build_freetype - fi - - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS=-lfreetype - export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/ - fi - build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no - if [ -z "$IS_MACOS" ]; then - export FREETYPE_LIBS='' - export FREETYPE_CFLAGS='' - fi - - # Append licenses - for filename in wheels/dependency_licenses/*; do - echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE - cat $filename >> LICENSE - done -} - -function pip_wheel_cmd { - local abs_wheelhouse=$1 - if [ -z "$IS_MACOS" ]; then - CFLAGS="$CFLAGS --std=c99" # for Raqm - fi - python3 -m pip wheel $(pip_opts) \ - -C raqm=enable -C raqm=vendor -C fribidi=vendor \ - -w $abs_wheelhouse --no-deps . -} - -function run_tests_in_repo { - # Run Pillow tests from within source repo - python3 selftest.py - python3 -m pytest -} - -EXP_CODECS="jpg jpg_2000 libtiff zlib" -EXP_MODULES="freetype2 littlecms2 pil tkinter webp" -EXP_FEATURES="fribidi harfbuzz libjpeg_turbo raqm transp_webp webp_anim webp_mux xcb" - -function run_tests { - if [ -n "$IS_MACOS" ]; then - brew install fribidi - export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" - elif [ -n "$IS_ALPINE" ]; then - apk add curl fribidi - else - apt-get update - apt-get install -y curl libfribidi0 libopenblas-dev pkg-config unzip - fi - if [ -z "$IS_ALPINE" ]; then - python3 -m pip install numpy - fi - python3 -m pip install defusedxml olefile pyroma - - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - untar pillow-test-images.zip - mv test-images-main/* ../Tests/images - - # Runs tests on installed distribution from an empty directory - (cd .. && run_tests_in_repo) - # Test against expected codecs, modules and features - local ret=0 - local codecs=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_codecs())))') - if [ "$codecs" != "$EXP_CODECS" ]; then - echo "Codecs should be: '$EXP_CODECS'; but are '$codecs'" - ret=1 - fi - local modules=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_modules())))') - if [ "$modules" != "$EXP_MODULES" ]; then - echo "Modules should be: '$EXP_MODULES'; but are '$modules'" - ret=1 - fi - local features=$(python3 -c 'from PIL.features import *; print(" ".join(sorted(get_supported_features())))') - if [ "$features" != "$EXP_FEATURES" ]; then - echo "Features should be: '$EXP_FEATURES'; but are '$features'" - ret=1 - fi - return $ret -} diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt index cca8d8ce1..93efc6126 100644 --- a/wheels/dependency_licenses/FREETYPE2.txt +++ b/wheels/dependency_licenses/FREETYPE2.txt @@ -38,3 +38,615 @@ the 'Old MIT' license, compatible to the above two licenses. The MD5 checksum support (only used for debugging in development builds) is in the public domain. + +-------------------------------------------------------------------------- + + The FreeType Project LICENSE + ---------------------------- + + 2006-Jan-27 + + Copyright 1996-2002, 2006 by + David Turner, Robert Wilhelm, and Werner Lemberg + + + +Introduction +============ + + The FreeType Project is distributed in several archive packages; + some of them may contain, in addition to the FreeType font engine, + various tools and contributions which rely on, or relate to, the + FreeType Project. + + This license applies to all files found in such packages, and + which do not fall under their own explicit license. The license + affects thus the FreeType font engine, the test programs, + documentation and makefiles, at the very least. + + This license was inspired by the BSD, Artistic, and IJG + (Independent JPEG Group) licenses, which all encourage inclusion + and use of free software in commercial and freeware products + alike. As a consequence, its main points are that: + + o We don't promise that this software works. However, we will be + interested in any kind of bug reports. (`as is' distribution) + + o You can use this software for whatever you want, in parts or + full form, without having to pay us. (`royalty-free' usage) + + o You may not pretend that you wrote this software. If you use + it, or only parts of it, in a program, you must acknowledge + somewhere in your documentation that you have used the + FreeType code. (`credits') + + We specifically permit and encourage the inclusion of this + software, with or without modifications, in commercial products. + We disclaim all warranties covering The FreeType Project and + assume no liability related to The FreeType Project. + + + Finally, many people asked us for a preferred form for a + credit/disclaimer to use in compliance with this license. We thus + encourage you to use the following text: + + """ + Portions of this software are copyright © The FreeType + Project (www.freetype.org). All rights reserved. + """ + + Please replace with the value from the FreeType version you + actually use. + + +Legal Terms +=========== + +0. Definitions +-------------- + + Throughout this license, the terms `package', `FreeType Project', + and `FreeType archive' refer to the set of files originally + distributed by the authors (David Turner, Robert Wilhelm, and + Werner Lemberg) as the `FreeType Project', be they named as alpha, + beta or final release. + + `You' refers to the licensee, or person using the project, where + `using' is a generic term including compiling the project's source + code as well as linking it to form a `program' or `executable'. + This program is referred to as `a program using the FreeType + engine'. + + This license applies to all files distributed in the original + FreeType Project, including all source code, binaries and + documentation, unless otherwise stated in the file in its + original, unmodified form as distributed in the original archive. + If you are unsure whether or not a particular file is covered by + this license, you must contact us to verify this. + + The FreeType Project is copyright (C) 1996-2000 by David Turner, + Robert Wilhelm, and Werner Lemberg. All rights reserved except as + specified below. + +1. No Warranty +-------------- + + THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO + USE, OF THE FREETYPE PROJECT. + +2. Redistribution +----------------- + + This license grants a worldwide, royalty-free, perpetual and + irrevocable right and license to use, execute, perform, compile, + display, copy, create derivative works of, distribute and + sublicense the FreeType Project (in both source and object code + forms) and derivative works thereof for any purpose; and to + authorize others to exercise some or all of the rights granted + herein, subject to the following conditions: + + o Redistribution of source code must retain this license file + (`FTL.TXT') unaltered; any additions, deletions or changes to + the original files must be clearly indicated in accompanying + documentation. The copyright notices of the unaltered, + original files must be preserved in all copies of source + files. + + o Redistribution in binary form must provide a disclaimer that + states that the software is based in part of the work of the + FreeType Team, in the distribution documentation. We also + encourage you to put an URL to the FreeType web page in your + documentation, though this isn't mandatory. + + These conditions apply to any software derived from or based on + the FreeType Project, not just the unmodified files. If you use + our work, you must acknowledge us. However, no fee need be paid + to us. + +3. Advertising +-------------- + + Neither the FreeType authors and contributors nor you shall use + the name of the other for commercial, advertising, or promotional + purposes without specific prior written permission. + + We suggest, but do not require, that you use one or more of the + following phrases to refer to this software in your documentation + or advertising materials: `FreeType Project', `FreeType Engine', + `FreeType library', or `FreeType Distribution'. + + As you have not signed this license, you are not required to + accept it. However, as the FreeType Project is copyrighted + material, only this license, or another one contracted with the + authors, grants you the right to use, distribute, and modify it. + Therefore, by using, distributing, or modifying the FreeType + Project, you indicate that you understand and accept all the terms + of this license. + +4. Contacts +----------- + + There are two mailing lists related to FreeType: + + o freetype@nongnu.org + + Discusses general use and applications of FreeType, as well as + future and wanted additions to the library and distribution. + If you are looking for support, start in this list if you + haven't found anything to help you in the documentation. + + o freetype-devel@nongnu.org + + Discusses bugs, as well as engine internals, design issues, + specific licenses, porting, etc. + + Our home page can be found at + + https://www.freetype.org + + +--- end of FTL.TXT --- + +-------------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + +-------------------------------------------------------------------------- + +The following license details are part of `src/bdf/README`: + +``` +License +******* + +Copyright (C) 2001-2002 by Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*** Portions of the driver (that is, bdflib.c and bdf.h): + +Copyright 2000 Computing Research Labs, New Mexico State University +Copyright 2001-2002, 2011 Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE COMPUTING RESEARCH LAB OR NEW MEXICO STATE UNIVERSITY BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT +OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Credits +******* + +This driver is based on excellent Mark Leisher's bdf library. If you +find something good in this driver you should probably thank him, not +me. +``` + +The following license details are part of `src/pcf/README`: + +``` +License +******* + +Copyright (C) 2000 by Francesco Zappa Nardelli + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Credits +******* + +Keith Packard wrote the pcf driver found in XFree86. His work is at +the same time the specification and the sample implementation of the +PCF format. Undoubtedly, this driver is inspired from his work. +``` diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 4c47db1fb..c5c9441d4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -118,6 +118,12 @@ DEPS = { "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" ), + "patch": { + r"CMakeLists.txt": { + # libjpeg-turbo does not detect MSVC x86_arm64 cross-compiler correctly + 'if(MSVC_IDE AND CMAKE_GENERATOR_PLATFORM MATCHES "arm64")': "if({architecture} STREQUAL ARM64)", # noqa: E501 + }, + }, "build": [ *cmds_cmake( ("jpeg-static", "cjpeg-static", "djpeg-static"), @@ -148,9 +154,9 @@ DEPS = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.4.tar.gz/download", - "filename": "xz-5.4.4.tar.gz", - "dir": "xz-5.4.4", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.5.tar.gz/download", + "filename": "xz-5.4.5.tar.gz", + "dir": "xz-5.4.5", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), @@ -213,7 +219,6 @@ DEPS = { ], "headers": [r"libtiff\tiff*.h"], "libs": [r"libtiff\*.lib"], - # "bins": [r"libtiff\*.dll"], }, "libpng": { "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download", @@ -272,7 +277,6 @@ DEPS = { cmd_xcopy("include", "{inc_dir}"), ], "libs": [r"objs\{msbuild_arch}\Release Static\freetype.lib"], - # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", @@ -329,6 +333,8 @@ DEPS = { "CMakeLists.txt": { "if(OPENMP_FOUND)": "if(false)", "install": "#install", + # libimagequant does not detect MSVC x86_arm64 cross-compiler correctly + "if(${{CMAKE_SYSTEM_PROCESSOR}} STREQUAL ARM64)": "if({architecture} STREQUAL ARM64)", # noqa: E501 } }, "build": [ @@ -339,9 +345,9 @@ DEPS = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/8.2.1.zip", - "filename": "harfbuzz-8.2.1.zip", - "dir": "harfbuzz-8.2.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/8.3.0.zip", + "filename": "harfbuzz-8.3.0.zip", + "dir": "harfbuzz-8.3.0", "license": "COPYING", "build": [ *cmds_cmake( @@ -369,12 +375,17 @@ DEPS = { # based on distutils._msvccompiler from CPython 3.7.4 -def find_msvs() -> dict[str, str] | None: +def find_msvs(architecture: str) -> dict[str, str] | None: root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") if not root: print("Program Files not found") return None + if architecture == "ARM64": + tools = "Microsoft.VisualStudio.Component.VC.Tools.ARM64" + else: + tools = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" + try: vspath = ( subprocess.check_output( @@ -385,7 +396,7 @@ def find_msvs() -> dict[str, str] | None: "-latest", "-prerelease", "-requires", - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + tools, "-property", "installationPath", "-products", @@ -471,7 +482,7 @@ def extract_dep(url: str, filename: str) -> None: msg = "Attempted Path Traversal in Zip File" raise RuntimeError(msg) zf.extractall(sources_dir) - elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): + elif filename.endswith((".tar.gz", ".tgz")): with tarfile.open(file, "r:gz") as tgz: for member in tgz.getnames(): member_abspath = os.path.abspath(os.path.join(sources_dir, member)) @@ -575,14 +586,19 @@ def build_dep(name: str) -> str: def build_dep_all() -> None: lines = [r'call "{build_dir}\build_env.cmd"'] + gha_groups = "GITHUB_ACTIONS" in os.environ for dep_name in DEPS: print() if dep_name in disabled: print(f"Skipping disabled dependency {dep_name}") continue script = build_dep(dep_name) + if gha_groups: + lines.append(f"@echo ::group::Running {script}") lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + if gha_groups: + lines.append("@echo ::endgroup::") print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines) @@ -656,7 +672,7 @@ if __name__ == "__main__": arch_prefs = ARCHITECTURES[args.architecture] print("Target architecture:", args.architecture) - msvs = find_msvs() + msvs = find_msvs(args.architecture) if msvs is None: msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." raise RuntimeError(msg) @@ -691,6 +707,11 @@ if __name__ == "__main__": disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] + elif args.architecture == "ARM64" and platform.machine() != "ARM64": + import warnings + + warnings.warn("Cross-compiling FriBiDi is currently not supported, disabling") + disabled += ["fribidi"] prefs = { "architecture": args.architecture,