diff --git a/.appveyor.yml b/.appveyor.yml
index 60132a9a3..cc4d56d0b 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -21,13 +21,11 @@ environment:
install:
- '%PYTHON%\%EXECUTABLE% --version'
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
-- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
-- 7z x pillow-depends.zip -oc:\
- 7z x pillow-test-images.zip -oc:\
-- mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
-- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\
+- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
+- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.0.0.20230317
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- cd c:\pillow\winbuild\
diff --git a/.ci/install.sh b/.ci/install.sh
index 6e87d386d..4748feb3d 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
- sway wl-clipboard
+ sway wl-clipboard libopenblas-dev
fi
python3 -m pip install --upgrade pip
@@ -38,11 +38,10 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
- # TODO Remove condition when NumPy supports 3.12
- if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
+ python3 -m pip install numpy
# PyQt6 doesn't support PyPy3
- if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then
+ if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6
fi
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 81ba8ef15..844c7c1ec 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -28,7 +28,7 @@ jobs:
name: Docs
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 49611e287..78b80d26e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,7 +17,7 @@ jobs:
name: Lint
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: pre-commit cache
uses: actions/cache@v3
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index 1fc6262f4..a20838a15 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -3,6 +3,7 @@
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
python3 -m pip install coverage
@@ -13,8 +14,7 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-# TODO Remove condition when NumPy supports 3.12
-if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
+python3 -m pip install numpy
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
index 9e2fdc096..8fc7bd379 100644
--- a/.github/workflows/release-drafter.yml
+++ b/.github/workflows/release-drafter.yml
@@ -10,7 +10,7 @@ on:
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 24b8f85d1..31f63e1c6 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -8,7 +8,7 @@ on:
permissions:
issues: write
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index e7ab6466e..5caa9faa4 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -4,11 +4,19 @@ on:
push:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -36,7 +44,7 @@ jobs:
git config --global core.autocrlf input
- name: Checkout Pillow
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v4
@@ -76,17 +84,23 @@ jobs:
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
+ - name: Select Python version
+ run: |
+ ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
+
+ - name: Get latest NumPy version
+ id: latest-numpy
+ shell: bash.exe -eo pipefail -o igncr "{0}"
+ run: |
+ python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
+
- name: pip cache
uses: actions/cache@v3
with:
path: 'C:\cygwin\home\runneradmin\.cache\pip'
- key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
+ key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
restore-keys: |
- ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
-
- - name: Select Python version
- run: |
- ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
+ ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-
- name: Build system information
run: |
@@ -96,10 +110,10 @@ jobs:
run: |
bash.exe .ci/install.sh
- - name: Install a different NumPy
+ - name: Upgrade NumPy
shell: dash.exe -l "{0}"
run: |
- python3 -m pip install -U numpy
+ python3 -m pip install -U "numpy<1.26"
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 36d9c131d..98a5de158 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -4,11 +4,19 @@ on:
push:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -59,7 +67,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Build system information
run: python3 .github/workflows/system-info.py
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
index 36bb38cd7..76e02ae92 100644
--- a/.github/workflows/test-mingw.yml
+++ b/.github/workflows/test-mingw.yml
@@ -4,11 +4,19 @@ on:
push:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -34,7 +42,7 @@ jobs:
steps:
- name: Checkout Pillow
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up shell
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml
index 6fab0ecd2..21968ad5a 100644
--- a/.github/workflows/test-valgrind.yml
+++ b/.github/workflows/test-valgrind.yml
@@ -37,7 +37,7 @@ jobs:
name: ${{ matrix.docker }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Build system information
run: python3 .github/workflows/system-info.py
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 70afbab24..3d7ec8e67 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -4,11 +4,19 @@ on:
push:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -24,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-dev"]
+ python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"]
timeout-minutes: 30
@@ -32,16 +40,16 @@ jobs:
steps:
- name: Checkout Pillow
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Checkout cached dependencies
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: python-pillow/pillow-depends
path: winbuild\depends
- name: Checkout extra test images
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: python-pillow/test-images
path: Tests\test-images
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 893c0d12c..926fe2de6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,11 +4,19 @@ on:
push:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
pull_request:
paths-ignore:
- ".github/workflows/docs.yml"
+ - ".github/workflows/wheels*"
+ - ".gitmodules"
+ - ".travis.yml"
- "docs/**"
+ - "wheels/**"
workflow_dispatch:
permissions:
@@ -31,7 +39,7 @@ jobs:
python-version: [
"pypy3.10",
"pypy3.9",
- "3.12-dev",
+ "3.12",
"3.11",
"3.10",
"3.9",
@@ -48,7 +56,7 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
diff --git a/.github/workflows/wheels-build.sh b/.github/workflows/wheels-build.sh
new file mode 100755
index 000000000..0aeec6b96
--- /dev/null
+++ b/.github/workflows/wheels-build.sh
@@ -0,0 +1,40 @@
+#!/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-linux.yml b/.github/workflows/wheels-linux.yml
new file mode 100644
index 000000000..8b2d9d451
--- /dev/null
+++ b/.github/workflows/wheels-linux.yml
@@ -0,0 +1,69 @@
+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
new file mode 100644
index 000000000..c51abf39a
--- /dev/null
+++ b/.github/workflows/wheels-macos.yml
@@ -0,0 +1,57 @@
+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.yml b/.github/workflows/wheels.yml
new file mode 100644
index 000000000..4381a9856
--- /dev/null
+++ b/.github/workflows/wheels.yml
@@ -0,0 +1,42 @@
+name: Wheels
+
+on:
+ push:
+ paths:
+ - ".github/workflows/wheels*.yml"
+ - "wheels/*"
+ tags:
+ - "*"
+ pull_request:
+ paths:
+ - ".github/workflows/wheels*.yml"
+ - "wheels/*"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ macos:
+ uses: ./.github/workflows/wheels-macos.yml
+ with:
+ artifacts-name: "wheels"
+
+ linux:
+ uses: ./.github/workflows/wheels-linux.yml
+ with:
+ artifacts-name: "wheels"
+
+ success:
+ permissions:
+ contents: none
+ needs: [macos, linux]
+ runs-on: ubuntu-latest
+ name: Wheels Successful
+ steps:
+ - name: Success
+ run: echo Wheels Successful
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..80d5ab16c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "multibuild"]
+ path = wheels/multibuild
+ url = https://github.com/multi-build/multibuild.git
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 320a77f55..a8c7696df 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,12 @@
repos:
- - repo: https://github.com/psf/black
- rev: 23.3.0
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.13.0
+ hooks:
+ - id: pyupgrade
+ args: [--py38-plus]
+
+ - repo: https://github.com/psf/black-pre-commit-mirror
+ rev: 23.9.1
hooks:
- id: black
args: [--target-version=py38]
@@ -23,17 +29,17 @@ repos:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
- rev: v1.5.1
+ rev: v1.5.4
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/PyCQA/flake8
- rev: 6.0.0
+ rev: 6.1.0
hooks:
- id: flake8
additional_dependencies:
- [flake8-2020, flake8-errmsg, flake8-implicit-str-concat]
+ [flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
@@ -44,23 +50,28 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
+ - id: check-executables-have-shebangs
- id: check-merge-conflict
- id: check-json
- id: check-toml
- id: check-yaml
+ - id: end-of-file-fixer
+ exclude: ^Tests/images/
+ - id: trailing-whitespace
+ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v0.6.7
+ rev: v0.6.8
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 0.12.1
+ rev: 1.2.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.13
+ rev: v0.14
hooks:
- id: validate-pyproject
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..4c0320e87
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,135 @@
+if: tag IS present
+
+env:
+ global:
+ - CONFIG_PATH=wheels/config.sh
+ - REPO_DIR=.
+ - PLAT=aarch64
+ - TEST_DEPENDS=pytest-timeout
+
+language: python
+# Default Python version is usually 3.6
+python: "3.11"
+dist: focal
+services: docker
+
+jobs:
+ include:
+ - name: "3.8 Focal manylinux2014 aarch64"
+ os: linux
+ arch: arm64
+ env:
+ - MB_ML_VER=2014
+ - MB_PYTHON_VERSION=3.8
+ - name: "3.8 Focal 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"
+ 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
+
+install:
+ - build_multilinux aarch64 build_wheel
+ - ls -l "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/"
+
+script:
+ - install_run
+
+# Upload wheels to GitHub Releases
+deploy:
+ provider: releases
+ api_key: $GITHUB_RELEASE_TOKEN
+ file_glob: true
+ file: "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/*.whl"
+ on:
+ repo: python-pillow/Pillow
+ tags: true
+ skip_cleanup: true
diff --git a/CHANGES.rst b/CHANGES.rst
index 41faa1310..85547c464 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,9 +5,90 @@ Changelog (Pillow)
10.1.0 (unreleased)
-------------------
+- Allow saving I;16B images as PNG #7302
+ [radarhere]
+
+- Corrected drawing I;16 points and writing I;16 text #7257
+ [radarhere]
+
+- Set blue channel to 128 for BC5S #7413
+ [radarhere]
+
+- Increase flexibility when reading IPTC fields #7319
+ [radarhere]
+
+- Set C palette to be empty by default #7289
+ [radarhere]
+
+- Added gs_binary to control Ghostscript use on all platforms #7392
+ [radarhere]
+
+- Read bounding box information from the trailer of EPS files if specified #7382
+ [nopperl, radarhere]
+
+- Added reading 8-bit color DDS images #7426
+ [radarhere]
+
+- Added has_transparency_data #7420
+ [radarhere, hugovk]
+
+- Fixed bug when reading BC5S DDS images #7401
+ [radarhere]
+
+- Prevent TIFF orientation from being applied more than once #7383
+ [radarhere]
+
+- Use previous pixel alpha for QOI_OP_RGB #7357
+ [radarhere]
+
+- Added BC5U reading #7358
+ [radarhere]
+
+- Allow getpixel() to accept a list #7355
+ [radarhere, homm]
+
+- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336
+ [radarhere]
+
+- Expand JPEG buffer size when saving optimized or progressive #7345
+ [radarhere]
+
+- Added session type check for Linux in ImageGrab.grabclipboard() #7332
+ [TheNooB2706, radarhere, hugovk]
+
+- Allow "loop=None" when saving GIF images #7329
+ [radarhere]
+
+- Fixed transparency when saving P mode images to PDF #7323
+ [radarhere]
+
+- Added saving LA images as PDFs #7299
+ [radarhere]
+
+- Set SMaskInData to 1 for PDFs with alpha #7316, #7317
+ [radarhere]
+
+- Changed Image mode property to be read-only by default #7307
+ [radarhere]
+
+- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266
+ [mtreinish, radarhere]
+
+- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284
+ [radarhere]
+
- Fix missing symbols when libtiff depends on libjpeg #7270
[heitbaum]
+10.0.1 (2023-09-15)
+-------------------
+
+- Updated libwebp to 1.3.2 #7395
+ [radarhere]
+
+- Updated zlib to 1.3 #7344
+ [radarhere]
+
10.0.0 (2023-07-01)
-------------------
@@ -5735,8 +5816,8 @@ http://svn.effbot.org/public/pil/
a polyline, independent of line angle.
- Fixed bearing calculation and clipping in the ImageFont truetype
- renderer; this could lead to clipped text, or crashes in the low-
- level _imagingft module. (based on input from Adam Twardoch and
+ renderer; this could lead to clipped text, or crashes in the low-level
+ _imagingft module. (based on input from Adam Twardoch and
others).
- Added ImageQt wrapper module, for converting PIL Image objects to
@@ -5817,8 +5898,7 @@ http://svn.effbot.org/public/pil/
1.1.5c2 and 1.1.5 final
-----------------------
-- Added experimental PERSPECTIVE transform method (from Jeff Breiden-
- bach).
+- Added experimental PERSPECTIVE transform method (from Jeff Breidenbach).
1.1.5c1
-------
@@ -5890,8 +5970,8 @@ http://svn.effbot.org/public/pil/
- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA".
-- Added "getcolors()" method. This is similar to the existing histo-
- gram method, but looks at color values instead of individual layers,
+- Added "getcolors()" method. This is similar to the existing histogram
+ method, but looks at color values instead of individual layers,
and returns an unsorted list of (count, color) tuples.
By default, the method returns None if finds more than 256 colors.
@@ -6107,8 +6187,8 @@ http://svn.effbot.org/public/pil/
- Added limited support for "bitfield compression" in BMP files
and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This
- also fixes a problem with ImageGrab module when copying screen-
- dumps from the clipboard on 15/16/32-bit displays.
+ also fixes a problem with ImageGrab module when copying screendumps
+ from the clipboard on 15/16/32-bit displays.
- Added experimental WAL (Quake 2 textures) loader. To use this
loader, import WalImageFile and call the "open" method in that
@@ -6219,8 +6299,8 @@ http://svn.effbot.org/public/pil/
1.1.3 final
-----------
-- Made setup.py look for old versions of zlib. For some back-
- ground, see: https://zlib.net/advisory-2002-03-11.txt
+- Made setup.py look for old versions of zlib. For some background,
+ see: https://zlib.net/advisory-2002-03-11.txt
1.1.3c2
-------
@@ -6411,8 +6491,8 @@ http://svn.effbot.org/public/pil/
supports all major PIL image modes (including F and I).
- The ImageFile module now includes a Parser class, which can
- be used to incrementally decode an image file (while down-
- loading it from the net, for example). See the handbook for
+ be used to incrementally decode an image file (while downloading
+ it from the net, for example). See the handbook for
details.
- "show" now converts non-standard modes to "L" or "RGB" (as
@@ -6550,8 +6630,8 @@ http://svn.effbot.org/public/pil/
- The Image "transform" method now supports Image.QUAD transforms.
The data argument is an 8-tuple giving the upper left, lower
- left, lower right, and upper right corner of the source quadri-
- lateral. Also added Image.MESH transform which takes a list
+ left, lower right, and upper right corner of the source quadrilateral.
+ Also added Image.MESH transform which takes a list
of quadrilaterals.
- The Image "resize", "rotate", and "transform" methods now support
@@ -6776,8 +6856,8 @@ The test suite includes 400 individual tests.
neither "short", "int" nor "long" are 32-bit wide.
- Added file= and data= keyword arguments to PhotoImage and BitmapImage.
- This allows you to use them as drop-in replacements for the corre-
- sponding Tkinter classes.
+ This allows you to use them as drop-in replacements for the corresponding
+ Tkinter classes.
- Removed bogus references to the crack coder (ImagingCrack).
diff --git a/MANIFEST.in b/MANIFEST.in
index 606e7e074..2bbddefa3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -29,3 +29,4 @@ global-exclude .git*
global-exclude *.pyc
global-exclude *.so
prune .ci
+prune wheels
diff --git a/README.md b/README.md
index af1ca57c2..994146cd5 100644
--- a/README.md
+++ b/README.md
@@ -45,12 +45,12 @@ As of 2019, Pillow development is
-
-
+
+ src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels">
diff --git a/RELEASING.md b/RELEASING.md
index 604bb1b8c..0229dbbc1 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
-* [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions.
+* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them.
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@@ -99,17 +99,14 @@ Released as needed privately to individual vendors for critical security-related
## Binary Distributions
### macOS and Linux
-* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
+* [ ] Download 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
- git clone https://github.com/python-pillow/pillow-wheels
- cd pillow-wheels
- ./update-pillow-tag.sh [[release tag]]
- ```
-* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
- and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo:
- ```bash
- gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels
+ gh run download --dir dist
+ # select dist-x.y.z
```
+* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
+ and copy into `dist`.
### Windows
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py
old mode 100755
new mode 100644
diff --git a/Tests/fonts/DejaVuSans/LICENSE.txt b/Tests/fonts/DejaVuSans/LICENSE.txt
index 30516578f..be6a4d84c 100644
--- a/Tests/fonts/DejaVuSans/LICENSE.txt
+++ b/Tests/fonts/DejaVuSans/LICENSE.txt
@@ -37,4 +37,4 @@ The Font Software may be sold as part of a larger software package but no copy o
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
-Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr.
\ No newline at end of file
+Except as contained in this notice, the name of Tavmjong Bah shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from Tavmjong Bah. For further information, contact: tavmjong @ free . fr.
diff --git a/Tests/helper.py b/Tests/helper.py
index 69246bfcf..de5468d84 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -91,7 +91,7 @@ def assert_image_equal(a, b, msg=None):
if HAS_UPLOADER:
try:
url = test_image_results.upload(a, b)
- logger.error(f"Url for test images: {url}")
+ logger.error("URL for test images: %s", url)
except Exception:
pass
@@ -126,7 +126,7 @@ def assert_image_similar(a, b, epsilon, msg=None):
if HAS_UPLOADER:
try:
url = test_image_results.upload(a, b)
- logger.error(f"Url for test images: {url}")
+ logger.exception("URL for test images: %s", url)
except Exception:
pass
raise e
diff --git a/Tests/icc/LICENSE.txt b/Tests/icc/LICENSE.txt
index 7d289c331..7119461ed 100644
--- a/Tests/icc/LICENSE.txt
+++ b/Tests/icc/LICENSE.txt
@@ -22,4 +22,3 @@ and that the name of ICC shall not be used in advertising or publicity
pertaining to distribution of the software without specific, written
prior permission. ICC makes no representations about the suitability
of this software for any purpose.
-
diff --git a/Tests/images/bc5s.png b/Tests/images/bc5s.png
index 39d7811bf..5e7a1b95e 100644
Binary files a/Tests/images/bc5s.png and b/Tests/images/bc5s.png differ
diff --git a/Tests/images/bc5u.dds b/Tests/images/bc5u.dds
new file mode 100644
index 000000000..4e4773cd7
Binary files /dev/null and b/Tests/images/bc5u.dds differ
diff --git a/Tests/images/imagedraw_rectangle_I.png b/Tests/images/imagedraw_rectangle_I.png
index 4e94f6943..a75f12c2e 100644
Binary files a/Tests/images/imagedraw_rectangle_I.png and b/Tests/images/imagedraw_rectangle_I.png differ
diff --git a/Tests/images/negative_size.ppm b/Tests/images/negative_size.ppm
old mode 100755
new mode 100644
diff --git a/Tests/images/palette.dds b/Tests/images/palette.dds
new file mode 100644
index 000000000..7171cb1f2
Binary files /dev/null and b/Tests/images/palette.dds differ
diff --git a/Tests/images/zero_bb_eof_before_boundingbox.eps b/Tests/images/zero_bb_eof_before_boundingbox.eps
new file mode 100644
index 000000000..c268bffd1
Binary files /dev/null and b/Tests/images/zero_bb_eof_before_boundingbox.eps differ
diff --git a/Tests/images/zero_bb_trailer.eps b/Tests/images/zero_bb_trailer.eps
new file mode 100644
index 000000000..964889965
Binary files /dev/null and b/Tests/images/zero_bb_trailer.eps differ
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index dc111c38b..0526f550e 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -6,6 +6,7 @@ import packaging
import pytest
from PIL import Image, features
+from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True)
@@ -48,6 +49,7 @@ def test_fuzz_images(path):
fuzzers.disable_decompressionbomb_error()
+@skip_unless_feature("freetype2")
@pytest.mark.parametrize(
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
)
diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py
index 3bdd5177d..745364ddc 100644
--- a/Tests/test_box_blur.py
+++ b/Tests/test_box_blur.py
@@ -22,7 +22,7 @@ def test_imageops_box_blur():
def box_blur(image, radius=1, n=1):
- return image._new(image.im.box_blur(radius, n))
+ return image._new(image.im.box_blur((radius, radius), n))
def assert_image(im, data, delta=0):
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index cac4108a8..bb9af7967 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -16,6 +16,7 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
+TEST_FILE_BC5U = "Tests/images/bc5u.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
@@ -81,10 +82,18 @@ def test_sanity_ati1():
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
-def test_sanity_ati2():
- """Check ATI2 images can be opened"""
+@pytest.mark.parametrize(
+ "image_path",
+ (
+ TEST_FILE_ATI2,
+ # hexeditted to use BC5U FourCC
+ TEST_FILE_BC5U,
+ ),
+)
+def test_sanity_ati2_bc5u(image_path):
+ """Check ATI2 and BC5U images can be opened"""
- with Image.open(TEST_FILE_ATI2) as im:
+ with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
@@ -289,6 +298,11 @@ def test_dxt5_colorblock_alpha_issue_4142():
assert px[2] != 0
+def test_palette():
+ with Image.open("Tests/images/palette.dds") as im:
+ assert_image_equal_tofile(im, "Tests/images/transparent.gif")
+
+
def test_unimplemented_pixel_format():
with pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 26adfff87..259cf75c3 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -8,6 +8,7 @@ from .helper import (
assert_image_similar,
assert_image_similar_tofile,
hopper,
+ is_win32,
mark_if_feature_version,
skip_unless_feature,
)
@@ -98,6 +99,20 @@ def test_load():
assert im.load()[0, 0] == (255, 255, 255)
+def test_binary():
+ if HAS_GHOSTSCRIPT:
+ assert EpsImagePlugin.gs_binary is not None
+ else:
+ assert EpsImagePlugin.gs_binary is False
+
+ if not is_win32():
+ assert EpsImagePlugin.gs_windows_binary is None
+ elif not HAS_GHOSTSCRIPT:
+ assert EpsImagePlugin.gs_windows_binary is False
+ else:
+ assert EpsImagePlugin.gs_windows_binary is not None
+
+
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
@@ -404,3 +419,18 @@ def test_timeout(test_file):
with pytest.raises(Image.UnidentifiedImageError):
with Image.open(f):
pass
+
+
+def test_bounding_box_in_trailer():
+ # Check bounding boxes are parsed in the same way
+ # when specified in the header and the trailer
+ with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
+ FILE1
+ ) as header_image:
+ assert trailer_image.size == header_image.size
+
+
+def test_eof_before_bounding_box():
+ with pytest.raises(OSError):
+ with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
+ pass
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index f4a17264f..3c2e96356 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -205,14 +205,14 @@ def test_optimize_full_l():
def test_optimize_if_palette_can_be_reduced_by_half():
- with Image.open("Tests/images/test.colors.gif") as im:
- # Reduce dimensions because original is too big for _get_optimize()
- im = im.resize((591, 443))
- im_rgb = im.convert("RGB")
+ im = Image.new("P", (8, 1))
+ im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
+ for i in range(8):
+ im.putpixel((i, 0), (i + 1, 0, 0))
for optimize, colors in ((False, 256), (True, 8)):
out = BytesIO()
- im_rgb.save(out, "GIF", optimize=optimize)
+ im.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded:
assert len(reloaded.palette.palette) // 3 == colors
@@ -875,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
assert reread.info["duration"] == 8500
+def test_loop_none(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ im = Image.new("L", (100, 100), "#000")
+ im.save(out, loop=None)
+ with Image.open(out) as reread:
+ assert "loop" not in reread.info
+
+
def test_number_of_loops(tmp_path):
number_of_loops = 2
@@ -1086,6 +1094,21 @@ def test_transparent_optimize(tmp_path):
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
+def test_removed_transparency(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ im = Image.new("RGB", (256, 1))
+
+ for x in range(256):
+ im.putpixel((x, 0), (x, 0, 0))
+
+ im.info["transparency"] = (255, 255, 255)
+ with pytest.warns(UserWarning):
+ im.save(out)
+
+ with Image.open(out) as reloaded:
+ assert "transparency" not in reloaded.info
+
+
def test_rgb_transparency(tmp_path):
out = str(tmp_path / "temp.gif")
@@ -1157,18 +1180,17 @@ def test_palette_save_L(tmp_path):
def test_palette_save_P(tmp_path):
- # Pass in a different palette, then construct what the image would look like.
- # Forcing a non-straight grayscale palette.
-
- im = hopper("P")
- palette = bytes(255 - i // 3 for i in range(768))
+ im = Image.new("P", (1, 2))
+ im.putpixel((0, 1), 1)
out = str(tmp_path / "temp.gif")
- im.save(out, palette=palette)
+ im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
with Image.open(out) as reloaded:
- im.putpalette(palette)
- assert_image_equal(reloaded, im)
+ reloaded_rgb = reloaded.convert("RGB")
+
+ assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
+ assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
def test_palette_save_duplicate_entries(tmp_path):
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index 2d99528d3..d2edcfc27 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -1,5 +1,5 @@
import sys
-from io import StringIO
+from io import BytesIO, StringIO
from PIL import Image, IptcImagePlugin
@@ -30,6 +30,36 @@ def test_getiptcinfo_jpg_found():
assert iptc[(2, 101)] == b"Hungary"
+def test_getiptcinfo_fotostation():
+ # Arrange
+ with open(TEST_FILE, "rb") as fp:
+ data = bytearray(fp.read())
+ data[86] = 240
+ f = BytesIO(data)
+ with Image.open(f) as im:
+ # Act
+ iptc = IptcImagePlugin.getiptcinfo(im)
+
+ # Assert
+ for tag in iptc.keys():
+ if tag[0] == 240:
+ return
+ assert False, "FotoStation tag not found"
+
+
+def test_getiptcinfo_zero_padding():
+ # Arrange
+ with Image.open(TEST_FILE) as im:
+ im.info["photoshop"][0x0404] += b"\x00\x00\x00"
+
+ # Act
+ iptc = IptcImagePlugin.getiptcinfo(im)
+
+ # Assert
+ assert isinstance(iptc, dict)
+ assert len(iptc) == 3
+
+
def test_getiptcinfo_tiff_none():
# Arrange
with Image.open("Tests/images/hopper.tif") as im:
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 217ad74f8..4d350f5d2 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -214,13 +214,20 @@ class TestFileJpeg:
# Should not raise OSError for image with icc larger than image size.
im.save(
f,
- format="JPEG",
progressive=True,
quality=95,
icc_profile=icc_profile,
optimize=True,
)
+ with Image.open("Tests/images/flower2.jpg") as im:
+ f = str(tmp_path / "temp2.jpg")
+ im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955)
+
+ with Image.open("Tests/images/flower2.jpg") as im:
+ f = str(tmp_path / "temp3.jpg")
+ im.save(f, progressive=True, quality=94, exif=b" " * 43668)
+
def test_optimize(self):
im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), optimize=0)
@@ -945,11 +952,10 @@ class TestFileJpeg:
assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17)
- def test_repr_jpeg_error(self):
+ def test_repr_jpeg_error_returns_none(self):
im = hopper("F")
- with pytest.raises(ValueError):
- im._repr_jpeg_()
+ assert im._repr_jpeg_() is None
@pytest.mark.skipif(not is_win32(), reason="Windows only")
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index b6e8215f7..99df26fc9 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -274,17 +274,15 @@ def test_sgnd(tmp_path):
assert reloaded_signed.getpixel((0, 0)) == 128
-def test_rgba():
+@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
+def test_rgba(ext):
# Arrange
- with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
- with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2:
- # Act
- j2k.load()
- jp2.load()
+ with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
+ # Act
+ im.load()
- # Assert
- assert j2k.mode == "RGBA"
- assert jp2.mode == "RGBA"
+ # Assert
+ assert im.mode == "RGBA"
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index ac78b0869..a7394f1bf 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -8,7 +8,7 @@ from collections import namedtuple
import pytest
-from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features
+from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
from .helper import (
@@ -1035,7 +1035,18 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
+ assert 274 in im.tag_v2
+
im.load()
+ assert 274 not in im.tag_v2
+
+ assert_image_similar(base_im, im, 0.7)
+
+ def test_exif_transpose(self):
+ with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
+ for i in range(2, 9):
+ with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
+ im = ImageOps.exif_transpose(im)
assert_image_similar(base_im, im, 0.7)
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index 967f5c35e..ffc392d6b 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -43,8 +43,25 @@ def test_save(tmp_path, mode):
@skip_unless_feature("jpg_2000")
-def test_save_rgba(tmp_path):
- helper_save_as_pdf(tmp_path, "RGBA")
+@pytest.mark.parametrize("mode", ("LA", "RGBA"))
+def test_save_alpha(tmp_path, mode):
+ helper_save_as_pdf(tmp_path, mode)
+
+
+def test_p_alpha(tmp_path):
+ # Arrange
+ outfile = str(tmp_path / "temp.pdf")
+ with Image.open("Tests/images/pil123p.png") as im:
+ assert im.mode == "P"
+ assert isinstance(im.info["transparency"], bytes)
+
+ # Act
+ im.save(outfile)
+
+ # Assert
+ with open(outfile, "rb") as fp:
+ contents = fp.read()
+ assert b"\n/SMask " in contents
def test_monochrome(tmp_path):
@@ -57,8 +74,8 @@ def test_monochrome(tmp_path):
def test_unsupported_mode(tmp_path):
- im = hopper("LA")
- outfile = str(tmp_path / "temp_LA.pdf")
+ im = hopper("PA")
+ outfile = str(tmp_path / "temp_PA.pdf")
with pytest.raises(ValueError):
im.save(outfile)
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index c4db97905..eea9f73d8 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -79,7 +79,7 @@ class TestFilePng:
def test_sanity(self, tmp_path):
# internal version number
- assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
+ assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
test_file = str(tmp_path / "temp.png")
@@ -92,11 +92,11 @@ class TestFilePng:
assert im.format == "PNG"
assert im.get_format_mimetype() == "image/png"
- for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
+ for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
im = hopper(mode)
im.save(test_file)
with Image.open(test_file) as reloaded:
- if mode == "I;16":
+ if mode in ("I;16", "I;16B"):
reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im)
@@ -532,11 +532,10 @@ class TestFilePng:
assert repr_png.format == "PNG"
assert_image_equal(im, repr_png)
- def test_repr_png_error(self):
+ def test_repr_png_error_returns_none(self):
im = hopper("F")
- with pytest.raises(ValueError):
- im._repr_png_()
+ assert im._repr_png_() is None
def test_chunk_order(self, tmp_path):
with Image.open("Tests/images/icc_profile.png") as im:
diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py
index f33eada61..0a835dcf6 100644
--- a/Tests/test_file_qoi.py
+++ b/Tests/test_file_qoi.py
@@ -2,7 +2,7 @@ import pytest
from PIL import Image, QoiImagePlugin
-from .helper import assert_image_equal_tofile, assert_image_similar_tofile
+from .helper import assert_image_equal_tofile
def test_sanity():
@@ -18,7 +18,7 @@ def test_sanity():
assert im.size == (162, 150)
assert im.format == "QOI"
- assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03)
+ assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
def test_invalid_file():
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index a7b6c735a..30938e971 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -235,3 +235,13 @@ class TestFileWebp:
with Image.open(out_webp) as reloaded:
reloaded.load()
assert reloaded.info["duration"] == 1000
+
+ def test_roundtrip_rgba_palette(self, tmp_path):
+ temp_file = str(tmp_path / "temp.webp")
+ im = Image.new("RGBA", (1, 1)).convert("P")
+ assert im.mode == "P"
+ assert im.palette.mode == "RGBA"
+ im.save(temp_file)
+
+ with Image.open(temp_file) as im:
+ assert im.getpixel((0, 0)) == (0, 0, 0, 0)
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 85f9f7d02..83dac7080 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -135,6 +135,12 @@ class TestImage:
with pytest.raises(AttributeError):
im.size = (3, 4)
+ def test_set_mode(self):
+ im = Image.new("RGB", (1, 1))
+
+ with pytest.raises(AttributeError):
+ im.mode = "P"
+
def test_invalid_image(self):
im = io.BytesIO(b"")
with pytest.raises(UnidentifiedImageError):
@@ -632,8 +638,8 @@ class TestImage:
im.remap_palette(None)
def test_remap_palette_transparency(self):
- im = Image.new("P", (1, 2))
- im.putpixel((0, 1), 1)
+ im = Image.new("P", (1, 2), (0, 0, 0))
+ im.putpixel((0, 1), (255, 0, 0))
im.info["transparency"] = 0
im_remapped = im.remap_palette([1, 0])
@@ -655,15 +661,15 @@ class TestImage:
blank_p.palette = None
blank_pa.palette = None
- def _make_new(base_image, im, palette_result=None):
- new_im = base_image._new(im)
- assert new_im.mode == im.mode
- assert new_im.size == im.size
- assert new_im.info == base_image.info
+ def _make_new(base_image, image, palette_result=None):
+ new_image = base_image._new(image.im)
+ assert new_image.mode == image.mode
+ assert new_image.size == image.size
+ assert new_image.info == base_image.info
if palette_result is not None:
- assert new_im.palette.tobytes() == palette_result.tobytes()
+ assert new_image.palette.tobytes() == palette_result.tobytes()
else:
- assert new_im.palette is None
+ assert new_image.palette is None
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im_p, im, None)
@@ -900,6 +906,31 @@ class TestImage:
im = Image.new("RGB", size)
assert im.tobytes() == b""
+ def test_has_transparency_data(self):
+ for mode in ("1", "L", "P", "RGB"):
+ im = Image.new(mode, (1, 1))
+ assert not im.has_transparency_data
+
+ for mode in ("LA", "La", "PA", "RGBA", "RGBa"):
+ im = Image.new(mode, (1, 1))
+ assert im.has_transparency_data
+
+ # P mode with "transparency" info
+ with Image.open("Tests/images/first_frame_transparency.gif") as im:
+ assert "transparency" in im.info
+ assert im.has_transparency_data
+
+ # RGB mode with "transparency" info
+ with Image.open("Tests/images/rgb_trns.png") as im:
+ assert "transparency" in im.info
+ assert im.has_transparency_data
+
+ # P mode with RGBA palette
+ im = Image.new("RGBA", (1, 1)).convert("P")
+ assert im.mode == "P"
+ assert im.palette.mode == "RGBA"
+ assert im.has_transparency_data
+
def test_apply_transparency(self):
im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1))
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index c9db3aee7..f80bc9c10 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -213,6 +213,10 @@ class TestImageGetPixel(AccessTest):
def test_basic(self, mode):
self.check(mode)
+ def test_list(self):
+ im = hopper()
+ assert im.getpixel([0, 0]) == (20, 20, 70)
+
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize(
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 01a182cf1..f5775f09c 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
f = str(tmp_path / "temp.png")
im_l = im.convert("L")
- assert im_l.info["transparency"] == 1 # undone
+ assert im_l.info["transparency"] == 0
im_l.save(f)
im_rgb = im.convert("RGB")
- assert im_rgb.info["transparency"] == (0, 1, 2) # undone
+ assert im_rgb.info["transparency"] == (0, 0, 0)
im_rgb.save(f)
diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py
index 25b72298e..a7932a351 100644
--- a/Tests/test_image_filter.py
+++ b/Tests/test_image_filter.py
@@ -23,9 +23,12 @@ from .helper import assert_image_equal, hopper
ImageFilter.MinFilter,
ImageFilter.ModeFilter,
ImageFilter.GaussianBlur,
+ ImageFilter.GaussianBlur(0),
ImageFilter.GaussianBlur(5),
+ ImageFilter.GaussianBlur((2, 5)),
ImageFilter.BoxBlur(0),
ImageFilter.BoxBlur(5),
+ ImageFilter.BoxBlur((2, 5)),
ImageFilter.UnsharpMask,
ImageFilter.UnsharpMask(10),
),
@@ -185,12 +188,21 @@ def test_consistency_5x5(mode):
assert_image_equal(source.filter(kernel), reference)
-def test_invalid_box_blur_filter():
+@pytest.mark.parametrize(
+ "radius",
+ (
+ -2,
+ (-2, -2),
+ (-2, 2),
+ (2, -2),
+ ),
+)
+def test_invalid_box_blur_filter(radius):
with pytest.raises(ValueError):
- ImageFilter.BoxBlur(-2)
+ ImageFilter.BoxBlur(radius)
im = hopper()
box_blur_filter = ImageFilter.BoxBlur(2)
- box_blur_filter.radius = -2
+ box_blur_filter.radius = radius
with pytest.raises(ValueError):
im.filter(box_blur_filter)
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
index 665e08a7e..376553344 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]
assert im.palette.colors == {(1, 2, 3, 4): 0}
+
+
+def test_empty_palette():
+ im = Image.new("P", (1, 1))
+ assert im.getpalette() == []
+
+
+def test_undefined_palette_index():
+ im = Image.new("P", (1, 1), 3)
+ im.putpalette((1, 2, 3))
+ assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 7497fdc66..1650cfa44 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -586,6 +586,18 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
+def test_point_I16():
+ # Arrange
+ im = Image.new("I;16", (1, 1))
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.point((0, 0), fill=0x1234)
+
+ # Assert
+ assert im.getpixel((0, 0)) == 0x1234
+
+
@pytest.mark.parametrize("points", POINTS)
def test_polygon(points):
# Arrange
@@ -732,7 +744,7 @@ def test_rectangle_I16(bbox):
draw = ImageDraw.Draw(im)
# Act
- draw.rectangle(bbox, fill="black", outline="green")
+ draw.rectangle(bbox, outline=0xFFFF)
# Assert
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
@@ -1326,6 +1338,7 @@ def test_stroke_multiline():
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3)
+@skip_unless_feature("freetype2")
def test_setting_default_font():
# Arrange
im = Image.new("RGB", (100, 250))
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index 412bc10d9..ff75b8c2a 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -136,7 +136,7 @@ class TestImageFile:
class DummyImageFile(ImageFile.ImageFile):
def _open(self):
- self.mode = "RGB"
+ self._mode = "RGB"
self._size = (1, 1)
im = DummyImageFile(buf)
@@ -217,7 +217,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100
class MockImageFile(ImageFile.ImageFile):
def _open(self):
self.rawmode = "RGBA"
- self.mode = "RGBA"
+ self._mode = "RGBA"
self._size = (200, 200)
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 02622e721..343ecda82 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -141,7 +141,9 @@ def test_I16(font):
draw = ImageDraw.Draw(im)
txt = "Hello World!"
- draw.text((10, 10), txt, font=font)
+ draw.text((10, 10), txt, fill=0xFFFE, font=font)
+
+ assert im.getpixel((12, 14)) == 0xFFFE
target = "Tests/images/transparent_background_text_L.png"
assert_image_similar_tofile(im.convert("L"), target, 0.01)
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
index 2f6d05888..1c5d482bd 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -75,13 +75,13 @@ def test_pickle_la_mode_with_palette(tmp_path):
# Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
- im.mode = "LA"
+ im._mode = "LA"
with open(filename, "wb") as f:
pickle.dump(im, f, protocol)
with open(filename, "rb") as f:
loaded_im = pickle.load(f)
- im.mode = "PA"
+ im._mode = "PA"
assert im == loaded_im
@@ -112,6 +112,7 @@ def helper_assert_pickled_font_images(font1, font2):
assert_image_equal(im1, im2)
+@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_string(protocol):
# Arrange
@@ -125,6 +126,7 @@ def test_pickle_font_string(protocol):
helper_assert_pickled_font_images(font, unpickled_font)
+@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_file(tmp_path, protocol):
# Arrange
diff --git a/_custom_build/backend.py b/_custom_build/backend.py
old mode 100755
new mode 100644
diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh
index fd6000ee1..ab94875d8 100755
--- a/depends/install_imagequant.sh
+++ b/depends/install_imagequant.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# install libimagequant
-archive=libimagequant-4.2.0
+archive=libimagequant-4.2.1
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh
index 24c1f9c30..070ba23a1 100755
--- a/depends/install_raqm.sh
+++ b/depends/install_raqm.sh
@@ -11,4 +11,3 @@ pushd $archive
meson build --prefix=/usr && sudo ninja -C build install
popd
-
diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh
index 7d2c399df..37d9d1160 100755
--- a/depends/install_raqm_cmake.sh
+++ b/depends/install_raqm_cmake.sh
@@ -15,4 +15,3 @@ make && sudo make install
cd ..
popd
-
diff --git a/depends/install_webp.sh b/depends/install_webp.sh
index 4636aab43..6f867ab37 100755
--- a/depends/install_webp.sh
+++ b/depends/install_webp.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# install webp
-archive=libwebp-1.3.1
+archive=libwebp-1.3.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
diff --git a/depends/termux.sh b/depends/termux.sh
index 1acc09c44..d437029fd 100755
--- a/depends/termux.sh
+++ b/depends/termux.sh
@@ -2,4 +2,3 @@
pkg install -y python ndk-sysroot clang make \
libjpeg-turbo
-
diff --git a/docs/about.rst b/docs/about.rst
index 03829c133..872ac0ea6 100644
--- a/docs/about.rst
+++ b/docs/about.rst
@@ -12,7 +12,7 @@ The fork author's goal is to foster and support active development of PIL throug
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
-.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels
+.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
.. _GitHub: https://github.com/python-pillow/Pillow
.. _Python Package Index: https://pypi.org/project/Pillow/
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index 26451533e..61690410b 100644
--- a/docs/example/DdsImagePlugin.py
+++ b/docs/example/DdsImagePlugin.py
@@ -225,7 +225,7 @@ class DdsImageFile(ImageFile.ImageFile):
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
- self.mode = "RGBA"
+ self._mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index fbaca5ae7..d5d95d3ce 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -63,8 +63,35 @@ DDS
^^^
DDS is a popular container texture format used in video games and natively supported
-by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1,
-DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode.
+by DirectX.
+
+DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
+
+.. versionadded:: 3.4.0
+ DXT3 images can be read in ``RGB`` mode and DX10 images can be read in
+ ``RGB`` and ``RGBA`` mode.
+
+.. versionadded:: 6.0.0
+ Uncompressed ``RGBA`` images can be read.
+
+
+.. versionadded:: 8.3.0
+ BC5S images can be opened in ``RGB`` mode, and uncompressed ``RGB`` images
+ can be read. Uncompressed data can also be saved to image files.
+
+
+.. versionadded:: 9.3.0
+ ATI1 images can be opened in ``L`` mode and ATI2 images can be opened in
+ ``RGB`` mode.
+
+.. versionadded:: 9.4.0
+ Uncompressed ``L`` ("luminance") and ``LA`` images can be opened and saved.
+
+
+.. versionadded:: 10.1.0
+ BC5U can be read in ``RGB`` mode, and 8-bit color indexed images can be read
+ in ``P`` mode.
+
DIB
^^^
@@ -88,8 +115,13 @@ in ``L``, ``RGB`` and ``CMYK`` modes.
Loading
~~~~~~~
+To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it
+also searches for "gswin32c" and "gswin64c". To customise this behaviour,
+``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to
+use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use.
+
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
-method with the following parameters to affect how Ghostscript renders the EPS
+method with the following parameters to affect how Ghostscript renders the EPS.
**scale**
Affects the scale of the resultant rasterized image. If the EPS suggests
@@ -253,7 +285,7 @@ their :py:attr:`~PIL.Image.Image.info` values.
**loop**
Integer number of times the GIF should loop. 0 means that it will loop
- forever. By default, the image will not loop.
+ forever. If omitted or ``None``, the image will not loop.
**comment**
A comment about the image.
@@ -861,6 +893,10 @@ PPM
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
``RGB`` data.
+"Raw" (P4 to P6) formats can be read, and are used when writing.
+
+Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
+
SGI
^^^
@@ -1482,7 +1518,7 @@ files. Different encoding methods are used, depending on the image mode.
unavailable
* L, RGB and CMYK mode images use JPEG encoding
* P mode images use HEX encoding
-* RGBA mode images use JPEG2000 encoding
+* LA and RGBA mode images use JPEG2000 encoding
.. _pdf-saving:
diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst
index 75604e17a..ca16fccda 100644
--- a/docs/handbook/writing-your-own-image-plugin.rst
+++ b/docs/handbook/writing-your-own-image-plugin.rst
@@ -72,11 +72,11 @@ true color.
# mode setting
bits = int(header[3])
if bits == 1:
- self.mode = "1"
+ self._mode = "1"
elif bits == 8:
- self.mode = "L"
+ self._mode = "L"
elif bits == 24:
- self.mode = "RGB"
+ self._mode = "RGB"
else:
msg = "unknown number of bits"
raise SyntaxError(msg)
diff --git a/docs/index.rst b/docs/index.rst
index 418844ba7..99da614a4 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -37,12 +37,12 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 10,Yes,Yes,Yes,Yes,,,
-Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,,
-Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,
-Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,
-Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,
-Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes
+Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
+Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,,
+Pillow 10.0,,Yes,Yes,Yes,Yes,,,
+Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,,
+Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,,
+Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes,
+Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes,
+Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes
diff --git a/docs/older-versions.csv b/docs/older-versions.csv
index 6058f0524..aa696bc18 100644
--- a/docs/older-versions.csv
+++ b/docs/older-versions.csv
@@ -5,4 +5,4 @@ Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,,
Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,,
Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,,
Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,,
-Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes
\ No newline at end of file
+Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes
diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst
index 41d3b8fce..4281b182c 100644
--- a/docs/reference/Image.rst
+++ b/docs/reference/Image.rst
@@ -93,10 +93,14 @@ Generating images
Registering plugins
^^^^^^^^^^^^^^^^^^^
+.. autofunction:: preinit
+.. autofunction:: init
+
.. note::
- These functions are for use by plugin authors. Application authors can
- ignore them.
+ These functions are for use by plugin authors. They are called when a
+ plugin is loaded as part of :py:meth:`~preinit()` or :py:meth:`~init()`.
+ Application authors can ignore them.
.. autofunction:: register_open
.. autofunction:: register_mime
@@ -347,6 +351,8 @@ Instances of the :py:class:`Image` class have the following attributes:
.. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell`
+.. autoattribute:: PIL.Image.Image.has_transparency_data
+
Classes
-------
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index 31f63695e..95a40007b 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -538,7 +538,7 @@ Methods
It should be a `BCP 47 language code`_.
Requires libraqm.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
- :return: Width for horizontal, height for vertical text.
+ :return: Either width for horizontal text, or height for vertical text.
.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst
index 4cd629322..06acfc7af 100644
--- a/docs/releasenotes/10.0.0.rst
+++ b/docs/releasenotes/10.0.0.rst
@@ -206,4 +206,4 @@ Support reading signed 8-bit TIFF images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TIFF images with signed integer data, 8 bits per sample and a photometric
-interpretaton of BlackIsZero can now be read.
+interpretation of BlackIsZero can now be read.
diff --git a/docs/releasenotes/10.0.1.rst b/docs/releasenotes/10.0.1.rst
new file mode 100644
index 000000000..6ac30e7fc
--- /dev/null
+++ b/docs/releasenotes/10.0.1.rst
@@ -0,0 +1,14 @@
+10.0.1
+------
+
+Security
+========
+
+This release addresses :cve:`2023-4863`, by providing an updated install script and
+updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow
+in WebP.
+
+Updated tests to pass with latest zlib version
+==============================================
+
+The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail.
diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst
new file mode 100644
index 000000000..af995dc15
--- /dev/null
+++ b/docs/releasenotes/10.1.0.rst
@@ -0,0 +1,66 @@
+10.1.0
+------
+
+Backwards Incompatible Changes
+==============================
+
+Setting image mode
+^^^^^^^^^^^^^^^^^^
+
+If you attempt to set the mode of an image directly, e.g.
+``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is
+not about removing existing functionality, but instead about raising an
+explicit error to prevent later consequences. The ``convert`` method is the
+correct way to change an image's mode.
+
+Deprecations
+============
+
+TODO
+^^^^
+
+TODO
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+has_transparency_data
+^^^^^^^^^^^^^^^^^^^^^
+
+Images now have :py:attr:`~PIL.Image.Image.has_transparency_data` to indicate
+whether the image has transparency data, whether in the form of an alpha
+channel, a palette with an alpha channel, or a "transparency" key in the
+:py:attr:`~PIL.Image.Image.info` dictionary.
+
+Even if this attribute is true, the image might still appear solid, if all of
+the values shown within are opaque.
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+Added support for DDS 8-bit color indexed images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read PALETTEINDEXED8 DDS files as P mode images.
+
+Support reading signed 8-bit YCbCr TIFF images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+TIFF images with unsigned integer data, 8 bits per sample and a photometric
+interpretation of YCbCr can now be read.
diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst
index 67569d337..e8eada73c 100644
--- a/docs/releasenotes/3.0.0.rst
+++ b/docs/releasenotes/3.0.0.rst
@@ -49,4 +49,3 @@ The external dependencies on libjpeg and zlib are now required by default.
If the headers or libraries are not found, then installation will abort
with an error. This behaviour can be disabled with the ``--disable-libjpeg``
and ``--disable-zlib`` flags.
-
diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst
index 68a09a3c8..8845b976a 100644
--- a/docs/releasenotes/3.3.2.rst
+++ b/docs/releasenotes/3.3.2.rst
@@ -34,7 +34,3 @@ image size can lead to a smaller allocation than expected, leading to
arbitrary writes.
This issue was found by Cris Neckar at Divergent Security.
-
-
-
-
diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst
index 7aa3c1fbf..1b5757015 100644
--- a/docs/releasenotes/4.1.1.rst
+++ b/docs/releasenotes/4.1.1.rst
@@ -20,5 +20,3 @@ CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix
undefines PySlice_GetIndicesEx if it exists to restore compatibility
with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for
more details.
-
-
diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst
index c9e953da4..0730936fe 100644
--- a/docs/releasenotes/4.2.1.rst
+++ b/docs/releasenotes/4.2.1.rst
@@ -8,4 +8,3 @@ Fixed Windows PyPy Build
A change in the 4.2.0 cycle broke the Windows PyPy build. This has
been fixed, and PyPy is now part of the Windows CI matrix.
-
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index fe2658047..00c691a74 100644
--- a/docs/releasenotes/8.0.0.rst
+++ b/docs/releasenotes/8.0.0.rst
@@ -175,6 +175,3 @@ Dark theme for docs
^^^^^^^^^^^^^^^^^^^
The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
-
-
-
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 9bca98541..1b1c353fd 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,8 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 10.1.0
+ 10.0.1
10.0.0
9.5.0
9.4.0
diff --git a/setup.cfg b/setup.cfg
index 06e95d7cc..e560f9516 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,6 +16,7 @@ classifiers =
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
+ Programming Language :: Python :: 3.12
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Multimedia :: Graphics
diff --git a/setup.py b/setup.py
index baf7b9395..935166716 100755
--- a/setup.py
+++ b/setup.py
@@ -39,7 +39,7 @@ TIFF_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
-if sys.platform == "win32" and sys.version_info >= (3, 12):
+if sys.platform == "win32" and sys.version_info >= (3, 13):
import atexit
atexit.register(
diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py
index 075d46290..161954831 100644
--- a/src/PIL/BdfFontFile.py
+++ b/src/PIL/BdfFontFile.py
@@ -68,11 +68,11 @@ def bdf_char(f):
# followed by the width in x (BBw), height in y (BBh),
# and x and y displacement (BBxoff0, BByoff0)
# of the lower left corner from the origin of the character.
- width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()]
+ width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
# The word DWIDTH
# followed by the width in x and y of the character in device pixels.
- dwx, dwy = [int(p) for p in props["DWIDTH"].split()]
+ dwx, dwy = (int(p) for p in props["DWIDTH"].split())
bbox = (
(dwx, dwy),
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 0ca60ff24..398696d5c 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -266,7 +266,7 @@ class BlpImageFile(ImageFile.ImageFile):
msg = f"Bad BLP magic {repr(self.magic)}"
raise BLPFormatError(msg)
- self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
+ self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
@@ -419,9 +419,11 @@ class BLPEncoder(ImageFile.PyEncoder):
def _write_palette(self):
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
- for i in range(256):
+ for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a)
+ while len(data) < 256 * 4:
+ data += b"\x00" * 4
return data
def encode(self, bufsize):
@@ -442,7 +444,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data
-def _save(im, fp, filename, save_all=False):
+def _save(im, fp, filename):
if im.mode != "P":
msg = "Unsupported BLP image mode"
raise ValueError(msg)
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 5bda0a5b0..9abfd0b5b 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -163,7 +163,7 @@ class BmpImageFile(ImageFile.ImageFile):
offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values
- self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
+ self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None:
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
raise OSError(msg)
@@ -200,7 +200,7 @@ class BmpImageFile(ImageFile.ImageFile):
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
):
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
- self.mode = "RGBA" if "A" in raw_mode else self.mode
+ self._mode = "RGBA" if "A" in raw_mode else self.mode
elif (
file_info["bits"] in (24, 16)
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
@@ -214,7 +214,7 @@ class BmpImageFile(ImageFile.ImageFile):
raise OSError(msg)
elif file_info["compression"] == self.RAW:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
- raw_mode, self.mode = "BGRA", "RGBA"
+ raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (self.RLE8, self.RLE4):
decoder_name = "bmp_rle"
else:
@@ -245,10 +245,10 @@ class BmpImageFile(ImageFile.ImageFile):
# ------- If all colors are grey, white or black, ditch palette
if greyscale:
- self.mode = "1" if file_info["colors"] == 2 else "L"
+ self._mode = "1" if file_info["colors"] == 2 else "L"
raw_mode = self.mode
else:
- self.mode = "P"
+ self._mode = "P"
self.palette = ImagePalette.raw(
"BGRX" if padding == 4 else "BGR", palette
)
diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py
index 0425bbd75..eef25aa14 100644
--- a/src/PIL/BufrStubImagePlugin.py
+++ b/src/PIL/BufrStubImagePlugin.py
@@ -46,7 +46,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
self.fp.seek(offset)
# make something up
- self.mode = "F"
+ self._mode = "F"
self._size = 1, 1
loader = self._load()
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index a946daeaa..54f358c7f 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -13,7 +13,7 @@ Full text of the CC0 license:
import struct
from io import BytesIO
-from . import Image, ImageFile
+from . import Image, ImageFile, ImagePalette
from ._binary import o32le as o32
# Magic ("DDS ")
@@ -128,7 +128,7 @@ class DdsImageFile(ImageFile.ImageFile):
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
- self.mode = "RGBA"
+ self._mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
@@ -141,9 +141,9 @@ class DdsImageFile(ImageFile.ImageFile):
if pfflags & DDPF_LUMINANCE:
# Texture contains uncompressed L or LA data
if pfflags & DDPF_ALPHAPIXELS:
- self.mode = "LA"
+ self._mode = "LA"
else:
- self.mode = "L"
+ self._mode = "L"
self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))]
elif pfflags & DDPF_RGB:
@@ -153,10 +153,14 @@ class DdsImageFile(ImageFile.ImageFile):
if pfflags & DDPF_ALPHAPIXELS:
rawmode += masks[0xFF000000]
else:
- self.mode = "RGB"
+ self._mode = "RGB"
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
+ elif pfflags & DDPF_PALETTEINDEXED8:
+ self._mode = "P"
+ self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
+ self.tile = [("raw", (0, 0) + self.size, 0, "L")]
else:
data_start = header_size + 4
n = 0
@@ -172,15 +176,15 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"ATI1":
self.pixel_format = "BC4"
n = 4
- self.mode = "L"
- elif fourcc == b"ATI2":
+ self._mode = "L"
+ elif fourcc in (b"ATI2", b"BC5U"):
self.pixel_format = "BC5"
n = 5
- self.mode = "RGB"
+ self._mode = "RGB"
elif fourcc == b"BC5S":
self.pixel_format = "BC5S"
n = 5
- self.mode = "RGB"
+ self._mode = "RGB"
elif fourcc == b"DX10":
data_start += 20
# ignoring flags which pertain to volume textures and cubemaps
@@ -189,19 +193,19 @@ class DdsImageFile(ImageFile.ImageFile):
if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
self.pixel_format = "BC5"
n = 5
- self.mode = "RGB"
+ self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
self.pixel_format = "BC5S"
n = 5
- self.mode = "RGB"
+ self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
self.pixel_format = "BC6H"
n = 6
- self.mode = "RGB"
+ self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
self.pixel_format = "BC6HS"
n = 6
- self.mode = "RGB"
+ self._mode = "RGB"
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py
index 6b1b5947e..9b2fce0ac 100644
--- a/src/PIL/EpsImagePlugin.py
+++ b/src/PIL/EpsImagePlugin.py
@@ -37,33 +37,39 @@ from ._deprecate import deprecate
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
+gs_binary = None
gs_windows_binary = None
-if sys.platform.startswith("win"):
- import shutil
-
- for binary in ("gswin32c", "gswin64c", "gs"):
- if shutil.which(binary) is not None:
- gs_windows_binary = binary
- break
- else:
- gs_windows_binary = False
def has_ghostscript():
- if gs_windows_binary:
- return True
- if not sys.platform.startswith("win"):
- try:
- subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
- return True
- except OSError:
- # No Ghostscript
- pass
- return False
+ global gs_binary, gs_windows_binary
+ if gs_binary is None:
+ if sys.platform.startswith("win"):
+ if gs_windows_binary is None:
+ import shutil
+
+ for binary in ("gswin32c", "gswin64c", "gs"):
+ if shutil.which(binary) is not None:
+ gs_windows_binary = binary
+ break
+ else:
+ gs_windows_binary = False
+ gs_binary = gs_windows_binary
+ else:
+ try:
+ subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
+ gs_binary = "gs"
+ except OSError:
+ gs_binary = False
+ return gs_binary is not False
def Ghostscript(tile, size, fp, scale=1, transparency=False):
"""Render an image using Ghostscript"""
+ global gs_binary
+ if not has_ghostscript():
+ msg = "Unable to locate Ghostscript on paths"
+ raise OSError(msg)
# Unpack decoder tile
decoder, tile, offset, data = tile[0]
@@ -113,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
# Build Ghostscript command
command = [
- "gs",
+ gs_binary,
"-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch)
@@ -132,19 +138,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
"showpage",
]
- if gs_windows_binary is not None:
- if not gs_windows_binary:
- try:
- os.unlink(outfile)
- if infile_temp:
- os.unlink(infile_temp)
- except OSError:
- pass
-
- msg = "Unable to locate Ghostscript on paths"
- raise OSError(msg)
- command[0] = gs_windows_binary
-
# push data through Ghostscript
try:
startupinfo = None
@@ -227,13 +220,15 @@ class EpsImageFile(ImageFile.ImageFile):
# go to offset - start of "%!PS"
self.fp.seek(offset)
- self.mode = "RGB"
+ self._mode = "RGB"
self._size = None
byte_arr = bytearray(255)
bytes_mv = memoryview(byte_arr)
bytes_read = 0
- reading_comments = True
+ reading_header_comments = True
+ reading_trailer_comments = False
+ trailer_reached = False
def check_required_header_comments():
if "PS-Adobe" not in self.info:
@@ -243,6 +238,36 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg)
+ def _read_comment(s):
+ nonlocal reading_trailer_comments
+ try:
+ m = split.match(s)
+ except re.error as e:
+ msg = "not an EPS file"
+ raise SyntaxError(msg) from e
+
+ if m:
+ k, v = m.group(1, 2)
+ self.info[k] = v
+ if k == "BoundingBox":
+ if v == "(atend)":
+ reading_trailer_comments = True
+ elif not self._size or (
+ trailer_reached and reading_trailer_comments
+ ):
+ try:
+ # Note: The DSC spec says that BoundingBox
+ # fields should be integers, but some drivers
+ # put floating point values there anyway.
+ box = [int(float(i)) for i in v.split()]
+ self._size = box[2] - box[0], box[3] - box[1]
+ self.tile = [
+ ("eps", (0, 0) + self.size, offset, (length, box))
+ ]
+ except Exception:
+ pass
+ return True
+
while True:
byte = self.fp.read(1)
if byte == b"":
@@ -265,9 +290,9 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "not an EPS file"
raise SyntaxError(msg)
else:
- if reading_comments:
+ if reading_header_comments:
check_required_header_comments()
- reading_comments = False
+ reading_header_comments = False
# reset bytes_read so we can keep reading
# data until the end of the line
bytes_read = 0
@@ -275,7 +300,7 @@ class EpsImageFile(ImageFile.ImageFile):
bytes_read += 1
continue
- if reading_comments:
+ if reading_header_comments:
# Load EPS header
# if this line doesn't start with a "%",
@@ -283,33 +308,11 @@ class EpsImageFile(ImageFile.ImageFile):
# then we've reached the end of the header/comments
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
check_required_header_comments()
- reading_comments = False
+ reading_header_comments = False
continue
s = str(bytes_mv[:bytes_read], "latin-1")
-
- try:
- m = split.match(s)
- except re.error as e:
- msg = "not an EPS file"
- raise SyntaxError(msg) from e
-
- if m:
- k, v = m.group(1, 2)
- self.info[k] = v
- if k == "BoundingBox":
- try:
- # Note: The DSC spec says that BoundingBox
- # fields should be integers, but some drivers
- # put floating point values there anyway.
- box = [int(float(i)) for i in v.split()]
- self._size = box[2] - box[0], box[3] - box[1]
- self.tile = [
- ("eps", (0, 0) + self.size, offset, (length, box))
- ]
- except Exception:
- pass
- else:
+ if not _read_comment(s):
m = field.match(s)
if m:
k = m.group(1)
@@ -339,15 +342,15 @@ class EpsImageFile(ImageFile.ImageFile):
# data start identifier (the image data follows after a single line
# consisting only of this quoted value)
image_data_values = byte_arr[11:bytes_read].split(None, 7)
- columns, rows, bit_depth, mode_id = [
+ columns, rows, bit_depth, mode_id = (
int(value) for value in image_data_values[:4]
- ]
+ )
if bit_depth == 1:
- self.mode = "1"
+ self._mode = "1"
elif bit_depth == 8:
try:
- self.mode = self.mode_map[mode_id]
+ self._mode = self.mode_map[mode_id]
except ValueError:
break
else:
@@ -355,7 +358,18 @@ class EpsImageFile(ImageFile.ImageFile):
self._size = columns, rows
return
+ elif trailer_reached and reading_trailer_comments:
+ # Load EPS trailer
+ # if this line starts with "%%EOF",
+ # then we've reached the end of the file
+ if bytes_mv[:5] == b"%%EOF":
+ break
+
+ s = str(bytes_mv[:bytes_read], "latin-1")
+ _read_comment(s)
+ elif bytes_mv[:9] == b"%%Trailer":
+ trailer_reached = True
bytes_read = 0
check_required_header_comments()
@@ -391,7 +405,7 @@ class EpsImageFile(ImageFile.ImageFile):
# Load EPS via Ghostscript
if self.tile:
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
- self.mode = self.im.mode
+ self._mode = self.im.mode
self._size = self.im.size
self.tile = []
return Image.Image.load(self)
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index 1359aeb12..e0e51aaac 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -51,14 +51,14 @@ class FitsImageFile(ImageFile.ImageFile):
number_of_bits = int(headers[b"BITPIX"])
if number_of_bits == 8:
- self.mode = "L"
+ self._mode = "L"
elif number_of_bits == 16:
- self.mode = "I"
+ self._mode = "I"
# rawmode = "I;16S"
elif number_of_bits == 32:
- self.mode = "I"
+ self._mode = "I"
elif number_of_bits in (-32, -64):
- self.mode = "F"
+ self._mode = "F"
# rawmode = "F" if number_of_bits == -32 else "F;64F"
offset = math.ceil(self.fp.tell() / 2880) * 2880
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index f4e89a03e..8f641ece9 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -56,7 +56,7 @@ class FliImageFile(ImageFile.ImageFile):
self.is_animated = self.n_frames > 1
# image characteristics
- self.mode = "P"
+ self._mode = "P"
self._size = i16(s, 8), i16(s, 10)
# animation speed
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index 2450c67e9..a878cbfd2 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -106,7 +106,7 @@ class FpxImageFile(ImageFile.ImageFile):
# note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
- self.mode, self.rawmode = MODES[tuple(colors)]
+ self._mode, self.rawmode = MODES[tuple(colors)]
# load JPEG tables, if any
self.jpeg = {}
diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py
index c46b2f28b..c2e4ead71 100644
--- a/src/PIL/FtexImagePlugin.py
+++ b/src/PIL/FtexImagePlugin.py
@@ -77,7 +77,7 @@ class FtexImageFile(ImageFile.ImageFile):
self._size = struct.unpack("<2i", self.fp.read(8))
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
- self.mode = "RGB"
+ self._mode = "RGB"
# Only support single-format files.
# I don't know of any multi-format file.
@@ -90,7 +90,7 @@ class FtexImageFile(ImageFile.ImageFile):
data = self.fp.read(mipmap_size)
if format == Format.DXT1:
- self.mode = "RGBA"
+ self._mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, 1)]
elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py
index 994a6e8eb..ec6e9de6e 100644
--- a/src/PIL/GbrImagePlugin.py
+++ b/src/PIL/GbrImagePlugin.py
@@ -73,9 +73,9 @@ class GbrImageFile(ImageFile.ImageFile):
comment = self.fp.read(comment_length)[:-1]
if color_depth == 1:
- self.mode = "L"
+ self._mode = "L"
else:
- self.mode = "RGBA"
+ self._mode = "RGBA"
self._size = width, height
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index bafc43a19..3599994a8 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -51,7 +51,7 @@ class GdImageFile(ImageFile.ImageFile):
msg = "Not a valid GD 2.x .gd file"
raise SyntaxError(msg)
- self.mode = "L" # FIXME: "P"
+ self._mode = "L" # FIXME: "P"
self._size = i16(s, 2), i16(s, 4)
true_color = s[6]
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index cf2993e38..92074b0d4 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -304,11 +304,11 @@ class GifImageFile(ImageFile.ImageFile):
if frame == 0:
if self._frame_palette:
if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
- self.mode = "RGBA" if frame_transparency is not None else "RGB"
+ self._mode = "RGBA" if frame_transparency is not None else "RGB"
else:
- self.mode = "P"
+ self._mode = "P"
else:
- self.mode = "L"
+ self._mode = "L"
if not palette and self.global_palette:
from copy import copy
@@ -325,10 +325,10 @@ class GifImageFile(ImageFile.ImageFile):
if "transparency" in self.info:
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
- self.mode = "RGBA"
+ self._mode = "RGBA"
del self.info["transparency"]
else:
- self.mode = "RGB"
+ self._mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
def _rgb(color):
@@ -424,7 +424,7 @@ class GifImageFile(ImageFile.ImageFile):
self.im.putpalette(*self._frame_palette.getdata())
else:
self.im = None
- self.mode = temp_mode
+ self._mode = temp_mode
self._frame_palette = None
super().load_prepare()
@@ -434,9 +434,9 @@ class GifImageFile(ImageFile.ImageFile):
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0)
- self.mode = "RGBA"
+ self._mode = "RGBA"
else:
- self.mode = "RGB"
+ self._mode = "RGB"
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
return
if not self._prev_im:
@@ -449,7 +449,7 @@ class GifImageFile(ImageFile.ImageFile):
frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im
- self.mode = self.im.mode
+ self._mode = self.im.mode
if frame_im.mode == "RGBA":
self.im.paste(frame_im, self.dispose_extent, frame_im)
else:
@@ -683,11 +683,7 @@ def get_interlace(im):
def _write_local_header(fp, im, offset, flags):
transparent_color_exists = False
try:
- if "transparency" in im.encoderinfo:
- transparency = im.encoderinfo["transparency"]
- else:
- transparency = im.info["transparency"]
- transparency = int(transparency)
+ transparency = int(im.encoderinfo["transparency"])
except (KeyError, ValueError):
pass
else:
@@ -916,7 +912,7 @@ def _get_global_header(im, info):
info
and (
"transparency" in info
- or "loop" in info
+ or info.get("loop") is not None
or info.get("duration")
or info.get("comment")
)
@@ -941,7 +937,7 @@ def _get_global_header(im, info):
# Global Color Table
_get_header_palette(palette_bytes),
]
- if "loop" in info:
+ if info.get("loop") is not None:
header.append(
b"!"
+ o8(255) # extension intro
diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py
index 8a799f19c..c1c71da08 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -46,7 +46,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
self.fp.seek(offset)
# make something up
- self.mode = "F"
+ self._mode = "F"
self._size = 1, 1
loader = self._load()
diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py
index bba05ed65..c26b480ac 100644
--- a/src/PIL/Hdf5StubImagePlugin.py
+++ b/src/PIL/Hdf5StubImagePlugin.py
@@ -46,7 +46,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
self.fp.seek(offset)
# make something up
- self.mode = "F"
+ self._mode = "F"
self._size = 1, 1
loader = self._load()
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index 27cb89f73..0aa4f7a84 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -253,7 +253,7 @@ class IcnsImageFile(ImageFile.ImageFile):
def _open(self):
self.icns = IcnsFile(self.fp)
- self.mode = "RGBA"
+ self._mode = "RGBA"
self.info["sizes"] = self.icns.itersizes()
self.best_size = self.icns.bestsize()
self.size = (
@@ -305,7 +305,7 @@ class IcnsImageFile(ImageFile.ImageFile):
px = im.load()
self.im = im.im
- self.mode = im.mode
+ self._mode = im.mode
self.size = im.size
return px
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index a188f8fdc..0445a2ab2 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -330,7 +330,7 @@ class IcoImageFile(ImageFile.ImageFile):
im.load()
self.im = im.im
self.pyaccess = None
- self.mode = im.mode
+ self._mode = im.mode
if im.size != self.size:
warnings.warn("Image was not the expected size")
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 746743f65..b42ba7cac 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -205,7 +205,7 @@ class ImImageFile(ImageFile.ImageFile):
# Basic attributes
self._size = self.info[SIZE]
- self.mode = self.info[MODE]
+ self._mode = self.info[MODE]
# Skip forward to start of image data
while s and s[:1] != b"\x1A":
@@ -231,9 +231,9 @@ class ImImageFile(ImageFile.ImageFile):
self.lut = list(palette[:256])
else:
if self.mode in ["L", "P"]:
- self.mode = self.rawmode = "P"
+ self._mode = self.rawmode = "P"
elif self.mode in ["LA", "PA"]:
- self.mode = "PA"
+ self._mode = "PA"
self.rawmode = "PA;L"
self.palette = ImagePalette.raw("RGB;L", palette)
elif self.mode == "RGB":
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 6d3715a92..a79666d7a 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -298,7 +298,11 @@ _initialized = 0
def preinit():
- """Explicitly load standard file format drivers."""
+ """
+ Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.
+
+ It is called when opening or saving images.
+ """
global _initialized
if _initialized >= 1:
@@ -334,11 +338,6 @@ def preinit():
assert PngImagePlugin
except ImportError:
pass
- # try:
- # import TiffImagePlugin
- # assert TiffImagePlugin
- # except ImportError:
- # pass
_initialized = 1
@@ -347,6 +346,9 @@ def init():
"""
Explicitly initializes the Python Imaging Library. This function
loads all available file format drivers.
+
+ It is called when opening or saving images if :py:meth:`~preinit()` is
+ insufficient, and by :py:meth:`~PIL.features.pilinfo`.
"""
global _initialized
@@ -482,7 +484,7 @@ class Image:
# FIXME: take "new" parameters / other image?
# FIXME: turn mode and size into delegating properties?
self.im = None
- self.mode = ""
+ self._mode = ""
self._size = (0, 0)
self.palette = None
self.info = {}
@@ -502,10 +504,14 @@ class Image:
def size(self):
return self._size
+ @property
+ def mode(self):
+ return self._mode
+
def _new(self, im):
new = Image()
new.im = im
- new.mode = im.mode
+ new._mode = im.mode
new._size = im.size
if im.mode in ("P", "PA"):
if self.palette:
@@ -641,9 +647,8 @@ class Image:
b = io.BytesIO()
try:
self.save(b, image_format, **kwargs)
- except Exception as e:
- msg = f"Could not save to {image_format} for display"
- raise ValueError(msg) from e
+ except Exception:
+ return None
return b.getvalue()
def _repr_png_(self):
@@ -693,7 +698,7 @@ class Image:
Image.__init__(self)
info, mode, size, palette, data = state
self.info = info
- self.mode = mode
+ self._mode = mode
self._size = size
self.im = core.new(mode, size)
if mode in ("L", "LA", "P", "PA") and palette:
@@ -910,7 +915,7 @@ class Image:
self.load()
- has_transparency = self.info.get("transparency") is not None
+ has_transparency = "transparency" in self.info
if not mode and self.mode == "P":
# determine default mode
if self.palette:
@@ -1069,7 +1074,7 @@ class Image:
if mode == "P" and palette != Palette.ADAPTIVE:
from . import ImagePalette
- new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
+ new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
if delete_trns:
# crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"]
@@ -1526,6 +1531,24 @@ class Image:
rawmode = mode
return list(self.im.getpalette(mode, rawmode))
+ @property
+ def has_transparency_data(self) -> bool:
+ """
+ Determine if an image has transparency data, whether in the form of an
+ alpha channel, a palette with an alpha channel, or a "transparency" key
+ in the info dictionary.
+
+ Note the image might still appear solid, if all of the values shown
+ within are opaque.
+
+ :returns: A boolean.
+ """
+ return (
+ self.mode in ("LA", "La", "PA", "RGBA", "RGBa")
+ or (self.mode == "P" and self.palette.mode.endswith("A"))
+ or "transparency" in self.info
+ )
+
def apply_transparency(self):
"""
If a P mode image has a "transparency" key in the info dictionary,
@@ -1562,7 +1585,7 @@ class Image:
self.load()
if self.pyaccess:
return self.pyaccess.getpixel(xy)
- return self.im.getpixel(xy)
+ return self.im.getpixel(tuple(xy))
def getprojection(self):
"""
@@ -1840,7 +1863,7 @@ class Image:
raise ValueError from e # sanity check
self.im = im
self.pyaccess = None
- self.mode = self.im.mode
+ self._mode = self.im.mode
except KeyError as e:
msg = "illegal image mode"
raise ValueError(msg) from e
@@ -1918,7 +1941,7 @@ class Image:
if not isinstance(data, bytes):
data = bytes(data)
palette = ImagePalette.raw(rawmode, data)
- self.mode = "PA" if "A" in self.mode else "P"
+ self._mode = "PA" if "A" in self.mode else "P"
self.palette = palette
self.palette.mode = "RGB"
self.load() # install new palette
@@ -2026,7 +2049,7 @@ class Image:
mapping_palette = bytearray(new_positions)
m_im = self.copy()
- m_im.mode = "P"
+ m_im._mode = "P"
m_im.palette = ImagePalette.ImagePalette(
palette_mode, palette=mapping_palette * bands
@@ -2601,7 +2624,7 @@ class Image:
self.im = im.im
self._size = size
- self.mode = self.im.mode
+ self._mode = self.im.mode
self.readonly = 0
self.pyaccess = None
@@ -2997,7 +3020,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
if args == ():
args = mode, 0, 1
if args[0] in _MAPMODES:
- im = new(mode, (1, 1))
+ im = new(mode, (0, 0))
im = im._new(core.map_buffer(data, size, decoder_name, 0, args))
if mode == "P":
from . import ImagePalette
@@ -3404,8 +3427,12 @@ def register_open(id, factory, accept=None):
def register_mime(id, mimetype):
"""
- Registers an image MIME type. This function should not be used
- in application code.
+ Registers an image MIME type by populating ``Image.MIME``. This function
+ should not be used in application code.
+
+ ``Image.MIME`` provides a mapping from image format identifiers to mime
+ formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can
+ provide a different result for specific images.
:param id: An image format identifier.
:param mimetype: The image MIME type for this format.
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 33bc7cc2e..57268b8f5 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -157,7 +157,8 @@ class GaussianBlur(MultibandFilter):
approximates a Gaussian kernel. For details on accuracy see
- :param radius: Standard deviation of the Gaussian kernel.
+ :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two
+ numbers for x and y, or a single number for both.
"""
name = "GaussianBlur"
@@ -166,7 +167,12 @@ class GaussianBlur(MultibandFilter):
self.radius = radius
def filter(self, image):
- return image.gaussian_blur(self.radius)
+ xy = self.radius
+ if not isinstance(xy, (tuple, list)):
+ xy = (xy, xy)
+ if xy == (0, 0):
+ return image.copy()
+ return image.gaussian_blur(xy)
class BoxBlur(MultibandFilter):
@@ -176,21 +182,31 @@ class BoxBlur(MultibandFilter):
which runs in linear time relative to the size of the image
for any radius value.
- :param radius: Size of the box in one direction. Radius 0 does not blur,
- returns an identical image. Radius 1 takes 1 pixel
- in each direction, i.e. 9 pixels in total.
+ :param radius: Size of the box in a direction. Either a sequence of two numbers for
+ x and y, or a single number for both.
+
+ Radius 0 does not blur, returns an identical image.
+ Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total.
"""
name = "BoxBlur"
def __init__(self, radius):
- if radius < 0:
+ xy = radius
+ if not isinstance(xy, (tuple, list)):
+ xy = (xy, xy)
+ if xy[0] < 0 or xy[1] < 0:
msg = "radius must be >= 0"
raise ValueError(msg)
self.radius = radius
def filter(self, image):
- return image.box_blur(self.radius)
+ xy = self.radius
+ if not isinstance(xy, (tuple, list)):
+ xy = (xy, xy)
+ if xy == (0, 0):
+ return image.copy()
+ return image.box_blur(xy)
class UnsharpMask(MultibandFilter):
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 05828a72f..5d361db52 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -318,7 +318,7 @@ class FreeTypeFont:
`_
Requires libraqm.
- :return: Width for horizontal, height for vertical text.
+ :return: Either width for horizontal text, or height for vertical text.
"""
_string_length_check(text)
return self.font.getlength(text, mode, direction, features, language) / 64
@@ -563,14 +563,21 @@ class FreeTypeFont:
if start is None:
start = (0, 0)
im = None
+ size = None
- def fill(mode, size):
- nonlocal im
+ def fill(mode, im_size):
+ nonlocal im, size
+
+ size = im_size
+ if Image.MAX_IMAGE_PIXELS is not None:
+ pixels = max(1, size[0]) * max(1, size[1])
+ if pixels > 2 * Image.MAX_IMAGE_PIXELS:
+ return
im = Image.core.fill(mode, size)
return im
- size, offset = self.font.render(
+ offset = self.font.render(
text,
fill,
mode,
@@ -582,7 +589,6 @@ class FreeTypeFont:
ink,
start[0],
start[1],
- Image.MAX_IMAGE_PIXELS,
)
Image._decompression_bomb_check(size)
return im, offset
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 927033c60..bcfffc3dc 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -140,7 +140,14 @@ def grabclipboard():
return BmpImagePlugin.DibImageFile(data)
return None
else:
- if shutil.which("wl-paste"):
+ if os.getenv("WAYLAND_DISPLAY"):
+ session_type = "wayland"
+ elif os.getenv("DISPLAY"):
+ session_type = "x11"
+ else: # Session type check failed
+ session_type = None
+
+ if shutil.which("wl-paste") and session_type in ("wayland", None):
output = subprocess.check_output(["wl-paste", "-l"]).decode()
mimetypes = output.splitlines()
if "image/png" in mimetypes:
@@ -153,12 +160,13 @@ def grabclipboard():
args = ["wl-paste"]
if mimetype:
args.extend(["-t", mimetype])
- elif shutil.which("xclip"):
+ elif shutil.which("xclip") and session_type in ("x11", None):
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
else:
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
raise NotImplementedError(msg)
- p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ p = subprocess.run(args, capture_output=True)
err = p.stderr
if err:
msg = f"{args[0]} error: {err.strip().decode()}"
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index ac7d36b69..eb6bbe6c6 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -247,7 +247,7 @@ def eval(expression, _dict={}, **kw):
def scan(code):
for const in code.co_consts:
- if type(const) == type(compiled_code):
+ if type(const) is type(compiled_code):
scan(const)
for name in code.co_names:
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 17702778c..1231ad6eb 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -588,6 +588,7 @@ def exif_transpose(image, *, in_place=False):
with the transposition applied. If there is no transposition, a copy of the
image will be returned.
"""
+ image.load()
image_exif = image.getexif()
orientation = image_exif.get(ExifTags.Base.Orientation)
method = {
diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index ac267457b..d409fcd59 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -89,7 +89,7 @@ class ImtImageFile(ImageFile.ImageFile):
ysize = int(v)
self._size = xsize, ysize
elif k == b"pixel" and v == b"n8":
- self.mode = "L"
+ self._mode = "L"
#
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 4c47b55c1..316cd17c7 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -58,13 +58,13 @@ class IptcImageFile(ImageFile.ImageFile):
#
# get a IPTC field header
s = self.fp.read(5)
- if not len(s):
+ if not s.strip(b"\x00"):
return None, 0
tag = s[1], s[2]
# syntax
- if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9:
+ if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]:
msg = "invalid IPTC/NAA file"
raise SyntaxError(msg)
@@ -109,11 +109,11 @@ class IptcImageFile(ImageFile.ImageFile):
else:
id = 0
if layers == 1 and not component:
- self.mode = "L"
+ self._mode = "L"
elif layers == 3 and component:
- self.mode = "RGB"[id]
+ self._mode = "RGB"[id]
elif layers == 4 and component:
- self.mode = "CMYK"[id]
+ self._mode = "CMYK"[id]
# size
self._size = self.getint((3, 20)), self.getint((3, 30))
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 9309768ba..963d6c1a3 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -208,14 +208,14 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
sig = self.fp.read(4)
if sig == b"\xff\x4f\xff\x51":
self.codec = "j2k"
- self._size, self.mode = _parse_codestream(self.fp)
+ self._size, self._mode = _parse_codestream(self.fp)
else:
sig = sig + self.fp.read(8)
if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
self.codec = "jp2"
header = _parse_jp2_header(self.fp)
- self._size, self.mode, self.custom_mimetype, dpi = header
+ self._size, self._mode, self.custom_mimetype, dpi = header
if dpi is not None:
self.info["dpi"] = dpi
if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"):
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index a28cd0367..2bb10e1f6 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -208,11 +208,11 @@ def SOF(self, marker):
self.layers = s[5]
if self.layers == 1:
- self.mode = "L"
+ self._mode = "L"
elif self.layers == 3:
- self.mode = "RGB"
+ self._mode = "RGB"
elif self.layers == 4:
- self.mode = "CMYK"
+ self._mode = "CMYK"
else:
msg = f"cannot handle {self.layers}-layer images"
raise SyntaxError(msg)
@@ -426,7 +426,7 @@ class JpegImageFile(ImageFile.ImageFile):
original_size = self.size
if a[0] == "RGB" and mode in ["L", "YCbCr"]:
- self.mode = mode
+ self._mode = mode
a = mode, ""
if size:
@@ -475,7 +475,7 @@ class JpegImageFile(ImageFile.ImageFile):
except OSError:
pass
- self.mode = self.im.mode
+ self._mode = self.im.mode
self._size = self.im.size
self.tile = []
@@ -797,10 +797,14 @@ def _save(im, fp, filename):
bufsize = 2 * im.size[0] * im.size[1]
else:
bufsize = im.size[0] * im.size[1]
-
- # The EXIF info needs to be written as one block, + APP1, + one spare byte.
- # Ensure that our buffer is big enough. Same with the icc_profile block.
- bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5, len(extra) + 1)
+ if exif:
+ bufsize += len(exif) + 5
+ if extra:
+ bufsize += len(extra) + 1
+ else:
+ # The EXIF info needs to be written as one block, + APP1, + one spare byte.
+ # Ensure that our buffer is big enough. Same with the icc_profile block.
+ bufsize = max(bufsize, len(exif) + 5, len(extra) + 1)
ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize)
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index 17c008b9a..bb79e71de 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -58,7 +58,7 @@ class McIdasImageFile(ImageFile.ImageFile):
msg = "unsupported McIdas format"
raise SyntaxError(msg)
- self.mode = mode
+ self._mode = mode
self._size = w[10], w[9]
offset = w[34] + w[15]
diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py
index d96d3a11c..bfa88fe99 100644
--- a/src/PIL/MpegImagePlugin.py
+++ b/src/PIL/MpegImagePlugin.py
@@ -68,7 +68,7 @@ class MpegImageFile(ImageFile.ImageFile):
msg = "not an MPEG file"
raise SyntaxError(msg)
- self.mode = "RGB"
+ self._mode = "RGB"
self._size = s.read(12), s.read(12)
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index c6567b2ae..3f3609f1c 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -62,7 +62,7 @@ class MspImageFile(ImageFile.ImageFile):
msg = "bad MSP checksum"
raise SyntaxError(msg)
- self.mode = "1"
+ self._mode = "1"
self._size = i16(s, 4), i16(s, 6)
if s[:4] == b"DanM":
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index e390f3fe5..c7cbca8c5 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -43,7 +43,7 @@ class PcdImageFile(ImageFile.ImageFile):
elif orientation == 3:
self.tile_post_rotate = -90
- self.mode = "RGB"
+ self._mode = "RGB"
self._size = 768, 512 # FIXME: not correct for rotated images!
self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)]
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index f42c2456b..854d9e83e 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -108,7 +108,7 @@ class PcxImageFile(ImageFile.ImageFile):
msg = "unknown PCX mode"
raise OSError(msg)
- self.mode = mode
+ self._mode = mode
self._size = bbox[2] - bbox[0], bbox[3] - bbox[1]
# Don't trust the passed in stride.
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index c41f8aee0..09fc0c7e6 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -46,6 +46,132 @@ def _save_all(im, fp, filename):
# (Internal) Image save plugin for the PDF format.
+def _write_image(im, filename, existing_pdf, image_refs):
+ # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
+ # (packbits) or LZWDecode (tiff/lzw compression). Note that
+ # PDF 1.2 also supports Flatedecode (zip compression).
+
+ params = None
+ decode = None
+
+ #
+ # Get image characteristics
+
+ width, height = im.size
+
+ dict_obj = {"BitsPerComponent": 8}
+ if im.mode == "1":
+ if features.check("libtiff"):
+ filter = "CCITTFaxDecode"
+ dict_obj["BitsPerComponent"] = 1
+ params = PdfParser.PdfArray(
+ [
+ PdfParser.PdfDict(
+ {
+ "K": -1,
+ "BlackIs1": True,
+ "Columns": width,
+ "Rows": height,
+ }
+ )
+ ]
+ )
+ else:
+ filter = "DCTDecode"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
+ procset = "ImageB" # grayscale
+ elif im.mode == "L":
+ filter = "DCTDecode"
+ # params = f"<< /Predictor 15 /Columns {width-2} >>"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
+ procset = "ImageB" # grayscale
+ elif im.mode == "LA":
+ filter = "JPXDecode"
+ # params = f"<< /Predictor 15 /Columns {width-2} >>"
+ procset = "ImageB" # grayscale
+ dict_obj["SMaskInData"] = 1
+ elif im.mode == "P":
+ filter = "ASCIIHexDecode"
+ palette = im.getpalette()
+ dict_obj["ColorSpace"] = [
+ PdfParser.PdfName("Indexed"),
+ PdfParser.PdfName("DeviceRGB"),
+ 255,
+ PdfParser.PdfBinary(palette),
+ ]
+ procset = "ImageI" # indexed color
+
+ if "transparency" in im.info:
+ smask = im.convert("LA").getchannel("A")
+ smask.encoderinfo = {}
+
+ image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0]
+ dict_obj["SMask"] = image_ref
+ elif im.mode == "RGB":
+ filter = "DCTDecode"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
+ procset = "ImageC" # color images
+ elif im.mode == "RGBA":
+ filter = "JPXDecode"
+ procset = "ImageC" # color images
+ dict_obj["SMaskInData"] = 1
+ elif im.mode == "CMYK":
+ filter = "DCTDecode"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
+ procset = "ImageC" # color images
+ decode = [1, 0, 1, 0, 1, 0, 1, 0]
+ else:
+ msg = f"cannot save mode {im.mode}"
+ raise ValueError(msg)
+
+ #
+ # image
+
+ op = io.BytesIO()
+
+ if filter == "ASCIIHexDecode":
+ ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
+ elif filter == "CCITTFaxDecode":
+ im.save(
+ op,
+ "TIFF",
+ compression="group4",
+ # use a single strip
+ strip_size=math.ceil(width / 8) * height,
+ )
+ elif filter == "DCTDecode":
+ Image.SAVE["JPEG"](im, op, filename)
+ elif filter == "JPXDecode":
+ del dict_obj["BitsPerComponent"]
+ Image.SAVE["JPEG2000"](im, op, filename)
+ else:
+ msg = f"unsupported PDF filter ({filter})"
+ raise ValueError(msg)
+
+ stream = op.getvalue()
+ if filter == "CCITTFaxDecode":
+ stream = stream[8:]
+ filter = PdfParser.PdfArray([PdfParser.PdfName(filter)])
+ else:
+ filter = PdfParser.PdfName(filter)
+
+ image_ref = image_refs.pop(0)
+ existing_pdf.write_obj(
+ image_ref,
+ stream=stream,
+ Type=PdfParser.PdfName("XObject"),
+ Subtype=PdfParser.PdfName("Image"),
+ Width=width, # * 72.0 / x_resolution,
+ Height=height, # * 72.0 / y_resolution,
+ Filter=filter,
+ Decode=decode,
+ DecodeParms=params,
+ **dict_obj,
+ )
+
+ return image_ref, procset
+
+
def _save(im, fp, filename, save_all=False):
is_appending = im.encoderinfo.get("append", False)
if is_appending:
@@ -109,6 +235,9 @@ def _save(im, fp, filename, save_all=False):
number_of_pages += im_number_of_pages
for i in range(im_number_of_pages):
image_refs.append(existing_pdf.next_object_id(0))
+ if im.mode == "P" and "transparency" in im.info:
+ image_refs.append(existing_pdf.next_object_id(0))
+
page_refs.append(existing_pdf.next_object_id(0))
contents_refs.append(existing_pdf.next_object_id(0))
existing_pdf.pages.append(page_refs[-1])
@@ -121,118 +250,7 @@ def _save(im, fp, filename, save_all=False):
for im_sequence in ims:
im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
for im in im_pages:
- # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
- # (packbits) or LZWDecode (tiff/lzw compression). Note that
- # PDF 1.2 also supports Flatedecode (zip compression).
-
- bits = 8
- params = None
- decode = None
-
- #
- # Get image characteristics
-
- width, height = im.size
-
- if im.mode == "1":
- if features.check("libtiff"):
- filter = "CCITTFaxDecode"
- bits = 1
- params = PdfParser.PdfArray(
- [
- PdfParser.PdfDict(
- {
- "K": -1,
- "BlackIs1": True,
- "Columns": width,
- "Rows": height,
- }
- )
- ]
- )
- else:
- filter = "DCTDecode"
- colorspace = PdfParser.PdfName("DeviceGray")
- procset = "ImageB" # grayscale
- elif im.mode == "L":
- filter = "DCTDecode"
- # params = f"<< /Predictor 15 /Columns {width-2} >>"
- colorspace = PdfParser.PdfName("DeviceGray")
- procset = "ImageB" # grayscale
- elif im.mode == "P":
- filter = "ASCIIHexDecode"
- palette = im.getpalette()
- colorspace = [
- PdfParser.PdfName("Indexed"),
- PdfParser.PdfName("DeviceRGB"),
- 255,
- PdfParser.PdfBinary(palette),
- ]
- procset = "ImageI" # indexed color
- elif im.mode == "RGB":
- filter = "DCTDecode"
- colorspace = PdfParser.PdfName("DeviceRGB")
- procset = "ImageC" # color images
- elif im.mode == "RGBA":
- filter = "JPXDecode"
- colorspace = PdfParser.PdfName("DeviceRGB")
- procset = "ImageC" # color images
- elif im.mode == "CMYK":
- filter = "DCTDecode"
- colorspace = PdfParser.PdfName("DeviceCMYK")
- procset = "ImageC" # color images
- decode = [1, 0, 1, 0, 1, 0, 1, 0]
- else:
- msg = f"cannot save mode {im.mode}"
- raise ValueError(msg)
-
- #
- # image
-
- op = io.BytesIO()
-
- if filter == "ASCIIHexDecode":
- ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
- elif filter == "CCITTFaxDecode":
- im.save(
- op,
- "TIFF",
- compression="group4",
- # use a single strip
- strip_size=math.ceil(im.width / 8) * im.height,
- )
- elif filter == "DCTDecode":
- Image.SAVE["JPEG"](im, op, filename)
- elif filter == "JPXDecode":
- Image.SAVE["JPEG2000"](im, op, filename)
- elif filter == "FlateDecode":
- ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)])
- elif filter == "RunLengthDecode":
- ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)])
- else:
- msg = f"unsupported PDF filter ({filter})"
- raise ValueError(msg)
-
- stream = op.getvalue()
- if filter == "CCITTFaxDecode":
- stream = stream[8:]
- filter = PdfParser.PdfArray([PdfParser.PdfName(filter)])
- else:
- filter = PdfParser.PdfName(filter)
-
- existing_pdf.write_obj(
- image_refs[page_number],
- stream=stream,
- Type=PdfParser.PdfName("XObject"),
- Subtype=PdfParser.PdfName("Image"),
- Width=width, # * 72.0 / x_resolution,
- Height=height, # * 72.0 / y_resolution,
- Filter=filter,
- BitsPerComponent=bits,
- Decode=decode,
- DecodeParms=params,
- ColorSpace=colorspace,
- )
+ image_ref, procset = _write_image(im, filename, existing_pdf, image_refs)
#
# page
@@ -241,13 +259,13 @@ def _save(im, fp, filename, save_all=False):
page_refs[page_number],
Resources=PdfParser.PdfDict(
ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
- XObject=PdfParser.PdfDict(image=image_refs[page_number]),
+ XObject=PdfParser.PdfDict(image=image_ref),
),
MediaBox=[
0,
0,
- width * 72.0 / x_resolution,
- height * 72.0 / y_resolution,
+ im.width * 72.0 / x_resolution,
+ im.height * 72.0 / y_resolution,
],
Contents=contents_refs[page_number],
)
@@ -256,8 +274,8 @@ def _save(im, fp, filename, save_all=False):
# page contents
page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
- width * 72.0 / x_resolution,
- height * 72.0 / y_resolution,
+ im.width * 72.0 / x_resolution,
+ im.height * 72.0 / y_resolution,
)
existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py
index 7eb82228a..850272311 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -54,7 +54,7 @@ class PixarImageFile(ImageFile.ImageFile):
mode = i16(s, 424), i16(s, 426)
if mode == (14, 2):
- self.mode = "RGB"
+ self._mode = "RGB"
# FIXME: to be continued...
# create tile descriptor (assuming "dumped")
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index bfa8cb7ac..2c7ae68d5 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -737,7 +737,7 @@ class PngImageFile(ImageFile.ImageFile):
# difficult to break if things go wrong in the decoder...
# (believe me, I've tried ;-)
- self.mode = self.png.im_mode
+ self._mode = self.png.im_mode
self._size = self.png.im_size
self.info = self.png.im_info
self._text = None
@@ -1042,6 +1042,7 @@ _OUTMODES = {
"LA": ("LA", b"\x08\x04"),
"I": ("I;16B", b"\x10\x00"),
"I;16": ("I;16B", b"\x10\x00"),
+ "I;16B": ("I;16B", b"\x10\x00"),
"P;1": ("P;1", b"\x01\x03"),
"P;2": ("P;2", b"\x02\x03"),
"P;4": ("P;4", b"\x04\x03"),
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 2cb1e5636..e480ab055 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -116,18 +116,18 @@ class PpmImageFile(ImageFile.ImageFile):
elif ix == 1: # token is the y size
ysize = token
if mode == "1":
- self.mode = "1"
+ self._mode = "1"
rawmode = "1;I"
break
else:
- self.mode = rawmode = mode
+ self._mode = rawmode = mode
elif ix == 2: # token is maxval
maxval = token
if not 0 < maxval < 65536:
msg = "maxval must be greater than 0 and less than 65536"
raise ValueError(msg)
if maxval > 255 and mode == "L":
- self.mode = "I"
+ self._mode = "I"
if decoder_name != "ppm_plain":
# If maxval matches a bit depth, use the raw decoder directly
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index 5a5d60d56..2f019bb8c 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -79,7 +79,7 @@ class PsdImageFile(ImageFile.ImageFile):
mode = "RGBA"
channels = 4
- self.mode = mode
+ self._mode = mode
self._size = i32(s, 18), i32(s, 14)
#
@@ -146,7 +146,7 @@ class PsdImageFile(ImageFile.ImageFile):
# seek to given layer (1..max)
try:
name, mode, bbox, tile = self.layers[layer - 1]
- self.mode = mode
+ self._mode = mode
self.tile = tile
self.frame = layer
self.fp = self._fp
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
index ef91b90ab..66344faac 100644
--- a/src/PIL/QoiImagePlugin.py
+++ b/src/PIL/QoiImagePlugin.py
@@ -29,7 +29,7 @@ class QoiImageFile(ImageFile.ImageFile):
self._size = tuple(i32(self.fp.read(4)) for i in range(2))
channels = self.fp.read(1)[0]
- self.mode = "RGB" if channels == 3 else "RGBA"
+ self._mode = "RGB" if channels == 3 else "RGBA"
self.fp.seek(1, os.SEEK_CUR) # colorspace
self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)]
@@ -55,7 +55,7 @@ class QoiDecoder(ImageFile.PyDecoder):
while len(data) < self.state.xsize * self.state.ysize * bands:
byte = self.fd.read(1)[0]
if byte == 0b11111110: # QOI_OP_RGB
- value = self.fd.read(3) + o8(255)
+ value = self.fd.read(3) + self._previous_pixel[3:]
elif byte == 0b11111111: # QOI_OP_RGBA
value = self.fd.read(4)
else:
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index 3662ffd15..acb9ce5a3 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -94,7 +94,7 @@ class SgiImageFile(ImageFile.ImageFile):
raise ValueError(msg)
self._size = xsize, ysize
- self.mode = rawmode.split(";")[0]
+ self._mode = rawmode.split(";")[0]
if self.mode == "RGB":
self.custom_mimetype = "image/rgb"
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 5614957c1..408b982b5 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -149,7 +149,7 @@ class SpiderImageFile(ImageFile.ImageFile):
self.rawmode = "F;32BF"
else:
self.rawmode = "F;32F"
- self.mode = "F"
+ self._mode = "F"
self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))]
self._fp = self.fp # FIXME: hack
diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py
index 6712583d7..6a8d5d86b 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -66,21 +66,21 @@ class SunImageFile(ImageFile.ImageFile):
palette_length = i32(s, 28)
if depth == 1:
- self.mode, rawmode = "1", "1;I"
+ self._mode, rawmode = "1", "1;I"
elif depth == 4:
- self.mode, rawmode = "L", "L;4"
+ self._mode, rawmode = "L", "L;4"
elif depth == 8:
- self.mode = rawmode = "L"
+ self._mode = rawmode = "L"
elif depth == 24:
if file_type == 3:
- self.mode, rawmode = "RGB", "RGB"
+ self._mode, rawmode = "RGB", "RGB"
else:
- self.mode, rawmode = "RGB", "BGR"
+ self._mode, rawmode = "RGB", "BGR"
elif depth == 32:
if file_type == 3:
- self.mode, rawmode = "RGB", "RGBX"
+ self._mode, rawmode = "RGB", "RGBX"
else:
- self.mode, rawmode = "RGB", "BGRX"
+ self._mode, rawmode = "RGB", "BGRX"
else:
msg = "Unsupported Mode/Bit Depth"
raise SyntaxError(msg)
@@ -97,7 +97,7 @@ class SunImageFile(ImageFile.ImageFile):
offset = offset + palette_length
self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length))
if self.mode == "L":
- self.mode = "P"
+ self._mode = "P"
rawmode = rawmode.replace("L", "P")
# 16 bit boundaries on stride
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 67dfc3d3c..f24ee4f5c 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -76,17 +76,17 @@ class TgaImageFile(ImageFile.ImageFile):
# image mode
if imagetype in (3, 11):
- self.mode = "L"
+ self._mode = "L"
if depth == 1:
- self.mode = "1" # ???
+ self._mode = "1" # ???
elif depth == 16:
- self.mode = "LA"
+ self._mode = "LA"
elif imagetype in (1, 9):
- self.mode = "P"
+ self._mode = "P"
elif imagetype in (2, 10):
- self.mode = "RGB"
+ self._mode = "RGB"
if depth == 32:
- self.mode = "RGBA"
+ self._mode = "RGBA"
else:
msg = "unknown TGA mode"
raise SyntaxError(msg)
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index d51488285..dabf8dbfb 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -251,6 +251,8 @@ OPEN_INFO = {
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
(II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
+ (II, 6, (1,), 1, (8,), ()): ("L", "L"),
+ (MM, 6, (1,), 1, (8,), ()): ("L", "L"),
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
@@ -823,7 +825,7 @@ class ImageFileDirectory_v2(MutableMapping):
try:
unit_size, handler = self._load_dispatch[typ]
except KeyError:
- logger.debug(msg + f" - unsupported type {typ}")
+ logger.debug("%s - unsupported type %s", msg, typ)
continue # ignore unsupported type
size = count * unit_size
if size > (8 if self._bigtiff else 4):
@@ -880,7 +882,7 @@ class ImageFileDirectory_v2(MutableMapping):
if tag == STRIPOFFSETS:
stripoffsets = len(entries)
typ = self.tagtype.get(tag)
- logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}")
+ logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd:
if self._endian == "<":
@@ -929,7 +931,7 @@ class ImageFileDirectory_v2(MutableMapping):
# pass 2: write entries to file
for tag, typ, count, value, data in entries:
- logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}")
+ logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
result += self._pack("HHL4s", tag, typ, count, value)
# -- overwrite here for multi-page --
@@ -1098,8 +1100,8 @@ class TiffImageFile(ImageFile.ImageFile):
self._n_frames = None
logger.debug("*** TiffImageFile._open ***")
- logger.debug(f"- __first: {self.__first}")
- logger.debug(f"- ifh: {repr(ifh)}") # Use repr to avoid str(bytes)
+ logger.debug("- __first: %s", self.__first)
+ logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes)
# and load the first frame
self._seek(0)
@@ -1137,12 +1139,15 @@ class TiffImageFile(ImageFile.ImageFile):
msg = "no more images in TIFF file"
raise EOFError(msg)
logger.debug(
- f"Seeking to frame {frame}, on frame {self.__frame}, "
- f"__next {self.__next}, location: {self.fp.tell()}"
+ "Seeking to frame %s, on frame %s, __next %s, location: %s",
+ frame,
+ self.__frame,
+ self.__next,
+ self.fp.tell(),
)
self.fp.seek(self.__next)
self._frame_pos.append(self.__next)
- logger.debug("Loading tags, location: %s" % self.fp.tell())
+ logger.debug("Loading tags, location: %s", self.fp.tell())
self.tag_v2.load(self.fp)
if self.tag_v2.next in self._frame_pos:
# This IFD has already been processed
@@ -1203,20 +1208,6 @@ class TiffImageFile(ImageFile.ImageFile):
return super().load()
def load_end(self):
- if self._tile_orientation:
- method = {
- 2: Image.Transpose.FLIP_LEFT_RIGHT,
- 3: Image.Transpose.ROTATE_180,
- 4: Image.Transpose.FLIP_TOP_BOTTOM,
- 5: Image.Transpose.TRANSPOSE,
- 6: Image.Transpose.ROTATE_270,
- 7: Image.Transpose.TRANSVERSE,
- 8: Image.Transpose.ROTATE_90,
- }.get(self._tile_orientation)
- if method is not None:
- self.im = self.im.transpose(method)
- self._size = self.im.size
-
# allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below.
if not self.is_animated:
@@ -1233,6 +1224,10 @@ class TiffImageFile(ImageFile.ImageFile):
continue
exif.get_ifd(key)
+ ImageOps.exif_transpose(self, in_place=True)
+ if ExifTags.Base.Orientation in self.tag_v2:
+ del self.tag_v2[ExifTags.Base.Orientation]
+
def _load_libtiff(self):
"""Overload method triggered when we detect a compressed tiff
Calls out to libtiff"""
@@ -1340,18 +1335,18 @@ class TiffImageFile(ImageFile.ImageFile):
fillorder = self.tag_v2.get(FILLORDER, 1)
logger.debug("*** Summary ***")
- logger.debug(f"- compression: {self._compression}")
- logger.debug(f"- photometric_interpretation: {photo}")
- logger.debug(f"- planar_configuration: {self._planar_configuration}")
- logger.debug(f"- fill_order: {fillorder}")
- logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")
+ logger.debug("- compression: %s", self._compression)
+ logger.debug("- photometric_interpretation: %s", photo)
+ logger.debug("- planar_configuration: %s", self._planar_configuration)
+ logger.debug("- fill_order: %s", fillorder)
+ logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING))
# size
xsize = int(self.tag_v2.get(IMAGEWIDTH))
ysize = int(self.tag_v2.get(IMAGELENGTH))
self._size = xsize, ysize
- logger.debug(f"- size: {self.size}")
+ logger.debug("- size: %s", self.size)
sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
@@ -1407,16 +1402,16 @@ class TiffImageFile(ImageFile.ImageFile):
bps_tuple,
extra_tuple,
)
- logger.debug(f"format key: {key}")
+ logger.debug("format key: %s", key)
try:
- self.mode, rawmode = OPEN_INFO[key]
+ self._mode, rawmode = OPEN_INFO[key]
except KeyError as e:
logger.debug("- unsupported format")
msg = "unknown pixel mode"
raise SyntaxError(msg) from e
- logger.debug(f"- raw mode: {rawmode}")
- logger.debug(f"- pil mode: {self.mode}")
+ logger.debug("- raw mode: %s", rawmode)
+ logger.debug("- pil mode: %s", self.mode)
self.info["compression"] = self._compression
@@ -1457,11 +1452,11 @@ class TiffImageFile(ImageFile.ImageFile):
if fillorder == 2:
# Replace fillorder with fillorder=1
key = key[:3] + (1,) + key[4:]
- logger.debug(f"format key: {key}")
+ logger.debug("format key: %s", key)
# this should always work, since all the
# fillorder==2 modes have a corresponding
# fillorder=1 mode
- self.mode, rawmode = OPEN_INFO[key]
+ self._mode, rawmode = OPEN_INFO[key]
# libtiff always returns the bytes in native order.
# we're expecting image byte order. So, if the rawmode
# contains I;16, we need to convert from native to image
@@ -1542,8 +1537,6 @@ class TiffImageFile(ImageFile.ImageFile):
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
- self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation)
-
#
# --------------------------------------------------------------------
@@ -1622,7 +1615,7 @@ def _save(im, fp, filename):
info = exif
else:
info = {}
- logger.debug("Tiffinfo Keys: %s" % list(info))
+ logger.debug("Tiffinfo Keys: %s", list(info))
if isinstance(info, ImageFileDirectory_v1):
info = info.to_v2()
for key in info:
@@ -1755,7 +1748,7 @@ def _save(im, fp, filename):
ifd[JPEGQUALITY] = quality
logger.debug("Saving using libtiff encoder")
- logger.debug("Items: %s" % sorted(ifd.items()))
+ logger.debug("Items: %s", sorted(ifd.items()))
_fp = 0
if hasattr(fp, "fileno"):
try:
@@ -1823,7 +1816,7 @@ def _save(im, fp, filename):
if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
- logger.debug("Converted items: %s" % sorted(atts.items()))
+ logger.debug("Converted items: %s", sorted(atts.items()))
# libtiff always expects the bytes in native order.
# we're storing image byte order. So, if the rawmode
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index e4f47aa04..3d9f97f84 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile):
format_description = "Quake2 Texture"
def _open(self):
- self.mode = "P"
+ self._mode = "P"
# read header fields
header = self.fp.read(32 + 24 + 32 + 12)
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index ce8e05fcb..612fc0946 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -43,7 +43,7 @@ class WebPImageFile(ImageFile.ImageFile):
def _open(self):
if not _webp.HAVE_WEBPANIM:
# Legacy mode
- data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode(
+ data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode(
self.fp.read()
)
if icc_profile:
@@ -74,7 +74,7 @@ class WebPImageFile(ImageFile.ImageFile):
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
self.n_frames = frame_count
self.is_animated = self.n_frames > 1
- self.mode = "RGB" if mode == "RGBX" else mode
+ self._mode = "RGB" if mode == "RGBX" else mode
self.rawmode = mode
self.tile = []
@@ -330,12 +330,7 @@ def _save(im, fp, filename):
exact = 1 if im.encoderinfo.get("exact") else 0
if im.mode not in _VALID_WEBP_LEGACY_MODES:
- alpha = (
- "A" in im.mode
- or "a" in im.mode
- or (im.mode == "P" and "transparency" in im.info)
- )
- im = im.convert("RGBA" if alpha else "RGB")
+ im = im.convert("RGBA" if im.has_transparency_data else "RGB")
data = _webp.WebPEncode(
im.tobytes(),
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index 0ecab56a8..3e5fb0151 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -42,7 +42,7 @@ if hasattr(Image.core, "drawwmf"):
class WmfHandler:
def open(self, im):
- im.mode = "RGB"
+ im._mode = "RGB"
self.bbox = im.info["wmf_bbox"]
def load(self, im):
@@ -139,7 +139,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
msg = "Unsupported file format"
raise SyntaxError(msg)
- self.mode = "RGB"
+ self._mode = "RGB"
self._size = size
loader = self._load()
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index aa4a01f4e..eda60c5c5 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -65,7 +65,7 @@ class XVThumbImageFile(ImageFile.ImageFile):
# parse header line (already read)
s = s.strip().split()
- self.mode = "P"
+ self._mode = "P"
self._size = int(s[0]), int(s[1])
self.palette = ImagePalette.raw("RGB", PALETTE)
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index 3c12564c9..71cd57d74 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -60,7 +60,7 @@ class XbmImageFile(ImageFile.ImageFile):
if m.group("hotspot"):
self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot")))
- self.mode = "1"
+ self._mode = "1"
self._size = xsize, ysize
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py
index 5d5bdc3ed..8491d3b7e 100644
--- a/src/PIL/XpmImagePlugin.py
+++ b/src/PIL/XpmImagePlugin.py
@@ -98,7 +98,7 @@ class XpmImageFile(ImageFile.ImageFile):
msg = "cannot read this XPM file"
raise ValueError(msg)
- self.mode = "P"
+ self._mode = "P"
self.palette = ImagePalette.raw("RGB", b"".join(palette))
self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))]
diff --git a/src/_imaging.c b/src/_imaging.c
index e15cb89fc..95da2772d 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -1075,9 +1075,9 @@ _gaussian_blur(ImagingObject *self, PyObject *args) {
Imaging imIn;
Imaging imOut;
- float radius = 0;
+ float xradius, yradius;
int passes = 3;
- if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) {
+ if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &passes)) {
return NULL;
}
@@ -1087,7 +1087,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) {
return NULL;
}
- if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) {
+ if (!ImagingGaussianBlur(imOut, imIn, xradius, yradius, passes)) {
ImagingDelete(imOut);
return NULL;
}
@@ -2131,9 +2131,9 @@ _box_blur(ImagingObject *self, PyObject *args) {
Imaging imIn;
Imaging imOut;
- float radius;
+ float xradius, yradius;
int n = 1;
- if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) {
+ if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &n)) {
return NULL;
}
@@ -2143,7 +2143,7 @@ _box_blur(ImagingObject *self, PyObject *args) {
return NULL;
}
- if (!ImagingBoxBlur(imOut, imIn, radius, n)) {
+ if (!ImagingBoxBlur(imOut, imIn, xradius, yradius, n)) {
ImagingDelete(imOut);
return NULL;
}
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 2165fbc7a..64175de8b 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -815,7 +815,6 @@ font_render(FontObject *self, PyObject *args) {
float y_start = 0;
int width, height, x_offset, y_offset;
int horizontal_dir; /* is primary axis horizontal? */
- PyObject *max_image_pixels = Py_None;
/* render string into given buffer (the buffer *must* have
the right size, or this will crash) */
@@ -833,8 +832,7 @@ font_render(FontObject *self, PyObject *args) {
&anchor,
&foreground_ink_long,
&x_start,
- &y_start,
- &max_image_pixels)) {
+ &y_start)) {
return NULL;
}
@@ -879,15 +877,11 @@ font_render(FontObject *self, PyObject *args) {
width += stroke_width * 2 + ceil(x_start);
height += stroke_width * 2 + ceil(y_start);
- if (max_image_pixels != Py_None) {
- if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) {
- PyMem_Del(glyph_info);
- return Py_BuildValue("(ii)(ii)", width, height, 0, 0);
- }
- }
-
image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
- if (image == NULL) {
+ if (image == Py_None) {
+ PyMem_Del(glyph_info);
+ return Py_BuildValue("ii", 0, 0);
+ } else if (image == NULL) {
PyMem_Del(glyph_info);
return NULL;
}
@@ -898,7 +892,7 @@ font_render(FontObject *self, PyObject *args) {
y_offset -= stroke_width;
if (count == 0 || width == 0 || height == 0) {
PyMem_Del(glyph_info);
- return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset);
+ return Py_BuildValue("ii", x_offset, y_offset);
}
if (stroke_width) {
@@ -1116,7 +1110,7 @@ font_render(FontObject *self, PyObject *args) {
Py_DECREF(image);
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
- return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset);
+ return Py_BuildValue("ii", x_offset, y_offset);
glyph_error:
if (im->destroy) {
diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c
index f00939da0..dd0418696 100644
--- a/src/libImaging/Access.c
+++ b/src/libImaging/Access.c
@@ -46,22 +46,11 @@ add_item(const char *mode) {
/* fetch individual pixel */
static void
-get_pixel(Imaging im, int x, int y, void *color) {
+get_pixel_32_2bands(Imaging im, int x, int y, void *color) {
char *out = color;
-
- /* generic pixel access*/
-
- if (im->image8) {
- out[0] = im->image8[y][x];
- } else {
- UINT8 *p = (UINT8 *)&im->image32[y][x];
- if (im->type == IMAGING_TYPE_UINT8 && im->bands == 2) {
- out[0] = p[0];
- out[1] = p[3];
- return;
- }
- memcpy(out, p, im->pixelsize);
- }
+ UINT8 *p = (UINT8 *)&im->image32[y][x];
+ out[0] = p[0];
+ out[1] = p[3];
}
static void
@@ -127,15 +116,6 @@ get_pixel_32B(Imaging im, int x, int y, void *color) {
/* store individual pixel */
-static void
-put_pixel(Imaging im, int x, int y, const void *color) {
- if (im->image8) {
- im->image8[y][x] = *((UINT8 *)color);
- } else {
- memcpy(&im->image32[y][x], color, sizeof(INT32));
- }
-}
-
static void
put_pixel_8(Imaging im, int x, int y, const void *color) {
im->image8[y][x] = *((UINT8 *)color);
@@ -186,8 +166,8 @@ ImagingAccessInit() {
/* populate access table */
ADD("1", get_pixel_8, put_pixel_8);
ADD("L", get_pixel_8, put_pixel_8);
- ADD("LA", get_pixel, put_pixel);
- ADD("La", get_pixel, put_pixel);
+ ADD("LA", get_pixel_32_2bands, put_pixel_32);
+ ADD("La", get_pixel_32_2bands, put_pixel_32);
ADD("I", get_pixel_32, put_pixel_32);
ADD("I;16", get_pixel_16L, put_pixel_16L);
ADD("I;16L", get_pixel_16L, put_pixel_16L);
@@ -197,7 +177,7 @@ ImagingAccessInit() {
ADD("I;32B", get_pixel_32B, put_pixel_32B);
ADD("F", get_pixel_32, put_pixel_32);
ADD("P", get_pixel_8, put_pixel_8);
- ADD("PA", get_pixel, put_pixel);
+ ADD("PA", get_pixel_32_2bands, put_pixel_32);
ADD("RGB", get_pixel_32, put_pixel_32);
ADD("RGBA", get_pixel_32, put_pixel_32);
ADD("RGBa", get_pixel_32, put_pixel_32);
diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c
index a57b74b61..5e4296eeb 100644
--- a/src/libImaging/BcnDecode.c
+++ b/src/libImaging/BcnDecode.c
@@ -118,8 +118,8 @@ decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) {
if (sign == 1) {
bc5s_alpha b;
memcpy(&b, src, sizeof(bc5s_alpha));
- a0 = (b.a0 + 255) / 2;
- a1 = (b.a1 + 255) / 2;
+ a0 = b.a0 + 128;
+ a1 = b.a1 + 128;
lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16);
lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16);
} else {
@@ -850,10 +850,12 @@ decode_bcn(
DECODE_LOOP(3, 16, rgba);
DECODE_LOOP(4, 8, lum);
case 5:
+ {
+ int sign = strcmp(pixel_format, "BC5S") == 0 ? 1 : 0;
while (bytes >= 16) {
rgba col[16];
- memset(col, 0, 16 * sizeof(col[0]));
- decode_bc5_block(col, ptr, strcmp(pixel_format, "BC5S") == 0 ? 1 : 0);
+ memset(col, sign ? 128 : 0, 16 * sizeof(col[0]));
+ decode_bc5_block(col, ptr, sign);
put_block(im, state, (const char *)col, sizeof(col[0]), C);
ptr += 16;
bytes -= 16;
@@ -862,10 +864,13 @@ decode_bcn(
}
}
break;
+ }
case 6:
+ {
+ int sign = strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0;
while (bytes >= 16) {
rgba col[16];
- decode_bc6_block(col, ptr, strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0);
+ decode_bc6_block(col, ptr, sign);
put_block(im, state, (const char *)col, sizeof(col[0]), C);
ptr += 16;
bytes -= 16;
@@ -874,6 +879,7 @@ decode_bcn(
}
}
break;
+ }
DECODE_LOOP(7, 16, rgba);
#undef DECODE_LOOP
}
diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c
index 5afe7cf50..adf425d0d 100644
--- a/src/libImaging/BoxBlur.c
+++ b/src/libImaging/BoxBlur.c
@@ -230,14 +230,14 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) {
}
Imaging
-ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) {
+ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) {
int i;
Imaging imTransposed;
if (n < 1) {
return ImagingError_ValueError("number of passes must be greater than zero");
}
- if (radius < 0) {
+ if (xradius < 0 || yradius < 0) {
return ImagingError_ValueError("radius must be >= 0");
}
@@ -258,35 +258,45 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) {
return ImagingError_ModeError();
}
- imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize);
- if (!imTransposed) {
- return NULL;
- }
-
/* Apply blur in one dimension.
Use imOut as a destination at first pass,
then use imOut as a source too. */
- ImagingHorizontalBoxBlur(imOut, imIn, radius);
- for (i = 1; i < n; i++) {
- ImagingHorizontalBoxBlur(imOut, imOut, radius);
- }
- /* Transpose result for blur in another direction. */
- ImagingTranspose(imTransposed, imOut);
- /* Reuse imTransposed as a source and destination there. */
- for (i = 0; i < n; i++) {
- ImagingHorizontalBoxBlur(imTransposed, imTransposed, radius);
+ if (xradius != 0) {
+ ImagingHorizontalBoxBlur(imOut, imIn, xradius);
+ for (i = 1; i < n; i++) {
+ ImagingHorizontalBoxBlur(imOut, imOut, xradius);
+ }
}
- /* Restore original orientation. */
- ImagingTranspose(imOut, imTransposed);
+ if (yradius != 0) {
+ imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize);
+ if (!imTransposed) {
+ return NULL;
+ }
- ImagingDelete(imTransposed);
+ /* Transpose result for blur in another direction. */
+ ImagingTranspose(imTransposed, xradius == 0 ? imIn : imOut);
+
+ /* Reuse imTransposed as a source and destination there. */
+ for (i = 0; i < n; i++) {
+ ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius);
+ }
+ /* Restore original orientation. */
+ ImagingTranspose(imOut, imTransposed);
+
+ ImagingDelete(imTransposed);
+ }
+ if (xradius == 0 && yradius == 0) {
+ if (!ImagingCopy2(imOut, imIn)) {
+ return NULL;
+ }
+ }
return imOut;
}
-Imaging
-ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) {
+static float
+_gaussian_blur_radius(float radius, int passes) {
float sigma2, L, l, a;
sigma2 = radius * radius / passes;
@@ -299,5 +309,16 @@ ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) {
a = (2 * l + 1) * (l * (l + 1) - 3 * sigma2);
a /= 6 * (sigma2 - (l + 1) * (l + 1));
- return ImagingBoxBlur(imOut, imIn, l + a, passes);
+ return l + a;
+}
+
+Imaging
+ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) {
+ return ImagingBoxBlur(
+ imOut,
+ imIn,
+ _gaussian_blur_radius(xradius, passes),
+ _gaussian_blur_radius(yradius, passes),
+ passes
+ );
}
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index 7fe24a639..7677a81f7 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -1295,7 +1295,6 @@ topalette(
int alpha;
int x, y;
ImagingPalette palette = inpalette;
- ;
/* Map L or RGB/RGBX/RGBA to palette image */
if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) {
@@ -1307,7 +1306,14 @@ topalette(
if (palette == NULL) {
/* FIXME: make user configurable */
if (imIn->bands == 1) {
- palette = ImagingPaletteNew("RGB"); /* Initialised to grey ramp */
+ palette = ImagingPaletteNew("RGB");
+
+ palette->size = 256;
+ int i;
+ for (i = 0; i < 256; i++) {
+ palette->palette[i * 4] = palette->palette[i * 4 + 1] =
+ palette->palette[i * 4 + 2] = (UINT8)i;
+ }
} else {
palette = ImagingPaletteNewBrowser(); /* Standard colour cube */
}
diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c
index 82f290bd0..0ccf22d58 100644
--- a/src/libImaging/Draw.c
+++ b/src/libImaging/Draw.c
@@ -41,6 +41,7 @@
#define FLOOR(v) ((v) >= 0.0 ? (int)(v) : (int)floor(v))
#define INK8(ink) (*(UINT8 *)ink)
+#define INK16(ink) (*(UINT16 *)ink)
/*
* Rounds around zero (up=away from zero, down=towards zero)
@@ -68,8 +69,13 @@ static inline void
point8(Imaging im, int x, int y, int ink) {
if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) {
if (strncmp(im->mode, "I;16", 4) == 0) {
- im->image8[y][x * 2] = (UINT8)ink;
+#ifdef WORDS_BIGENDIAN
+ im->image8[y][x * 2] = (UINT8)(ink >> 8);
im->image8[y][x * 2 + 1] = (UINT8)ink;
+#else
+ im->image8[y][x * 2] = (UINT8)ink;
+ im->image8[y][x * 2 + 1] = (UINT8)(ink >> 8);
+#endif
} else {
im->image8[y][x] = (UINT8)ink;
}
@@ -631,13 +637,17 @@ DRAW draw32rgba = {point32rgba, hline32rgba, line32rgba, polygon32rgba};
/* Interface */
/* -------------------------------------------------------------------- */
-#define DRAWINIT() \
- if (im->image8) { \
- draw = &draw8; \
- ink = INK8(ink_); \
- } else { \
- draw = (op) ? &draw32rgba : &draw32; \
- memcpy(&ink, ink_, sizeof(ink)); \
+#define DRAWINIT() \
+ if (im->image8) { \
+ draw = &draw8; \
+ if (strncmp(im->mode, "I;16", 4) == 0) { \
+ ink = INK16(ink_); \
+ } else { \
+ ink = INK8(ink_); \
+ } \
+ } else { \
+ draw = (op) ? &draw32rgba : &draw32; \
+ memcpy(&ink, ink_, sizeof(ink)); \
}
int
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index 01f40ee7b..afcd2229b 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -309,7 +309,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn);
extern Imaging
ImagingFlipTopBottom(Imaging imOut, Imaging imIn);
extern Imaging
-ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes);
+ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes);
extern Imaging
ImagingGetBand(Imaging im, int band);
extern Imaging
@@ -376,7 +376,7 @@ ImagingTransform(
extern Imaging
ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold);
extern Imaging
-ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n);
+ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n);
extern Imaging
ImagingColorLUT3D_linear(
Imaging imOut,
diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c
index de8586706..3295373fd 100644
--- a/src/libImaging/Jpeg2KEncode.c
+++ b/src/libImaging/Jpeg2KEncode.c
@@ -490,6 +490,8 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
if (strcmp(im->mode, "RGBA") == 0) {
image->comps[3].alpha = 1;
+ } else if (strcmp(im->mode, "LA") == 0) {
+ image->comps[1].alpha = 1;
}
opj_set_error_handler(codec, j2k_error, context);
diff --git a/src/libImaging/Palette.c b/src/libImaging/Palette.c
index 71a095c2c..059d7b72a 100644
--- a/src/libImaging/Palette.c
+++ b/src/libImaging/Palette.c
@@ -39,11 +39,8 @@ ImagingPaletteNew(const char *mode) {
strncpy(palette->mode, mode, IMAGING_MODE_LENGTH - 1);
palette->mode[IMAGING_MODE_LENGTH - 1] = 0;
- /* Initialize to ramp */
- palette->size = 256;
+ palette->size = 0;
for (i = 0; i < 256; i++) {
- palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] =
- palette->palette[i * 4 + 2] = (UINT8)i;
palette->palette[i * 4 + 3] = 255; /* opaque */
}
@@ -62,16 +59,10 @@ ImagingPaletteNewBrowser(void) {
return NULL;
}
- /* Blank out unused entries */
/* FIXME: Add 10-level windows palette here? */
- for (i = 0; i < 10; i++) {
- palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] =
- palette->palette[i * 4 + 2] = 0;
- }
-
/* Simple 6x6x6 colour cube */
-
+ i = 10;
for (b = 0; b < 256; b += 51) {
for (g = 0; g < 256; g += 51) {
for (r = 0; r < 256; r += 51) {
@@ -82,15 +73,10 @@ ImagingPaletteNewBrowser(void) {
}
}
}
+ palette->size = i;
- /* Blank out unused entries */
/* FIXME: add 30-level greyscale wedge here? */
- for (; i < 256; i++) {
- palette->palette[i * 4 + 0] = palette->palette[i * 4 + 1] =
- palette->palette[i * 4 + 2] = 0;
- }
-
return palette;
}
diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c
index acf5202e5..6684b11ef 100644
--- a/src/libImaging/Paste.c
+++ b/src/libImaging/Paste.c
@@ -425,7 +425,7 @@ fill_mask_L(
*out = BLEND(*mask, *out, ink[0], tmp1);
if (strncmp(imOut->mode, "I;16", 4) == 0) {
out++;
- *out = BLEND(*mask, *out, ink[0], tmp1);
+ *out = BLEND(*mask, *out, ink[1], tmp1);
}
out++, mask++;
}
diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c
index 02a4a5c76..c84acb998 100644
--- a/src/libImaging/Quant.c
+++ b/src/libImaging/Quant.c
@@ -1825,6 +1825,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
free(newData);
+ imOut->palette->size = (int)paletteLength;
pp = imOut->palette->palette;
for (i = j = 0; i < (int)paletteLength; i++) {
@@ -1832,16 +1833,9 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
*pp++ = palette[i].c.g;
*pp++ = palette[i].c.b;
if (withAlpha) {
- *pp++ = palette[i].c.a;
- } else {
- *pp++ = 255;
+ *pp = palette[i].c.a;
}
- }
- for (; i < 256; i++) {
- *pp++ = 0;
- *pp++ = 0;
- *pp++ = 0;
- *pp++ = 255;
+ pp++;
}
if (withAlpha) {
diff --git a/src/libImaging/Sgi.h b/src/libImaging/Sgi.h
index 39dd68825..797e5cbf9 100644
--- a/src/libImaging/Sgi.h
+++ b/src/libImaging/Sgi.h
@@ -36,4 +36,4 @@ typedef struct {
/* image data size from file descriptor */
long bufsize;
-} SGISTATE;
\ No newline at end of file
+} SGISTATE;
diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c
index 643ced49f..2853ce903 100644
--- a/src/libImaging/UnsharpMask.c
+++ b/src/libImaging/UnsharpMask.c
@@ -36,7 +36,7 @@ ImagingUnsharpMask(
/* First, do a gaussian blur on the image, putting results in imOut
temporarily. All format checks are in gaussian blur. */
- result = ImagingGaussianBlur(imOut, imIn, radius, 3);
+ result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3);
if (!result) {
return NULL;
}
diff --git a/tox.ini b/tox.ini
index a79089f51..5388ed243 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,7 @@ requires =
tox>=4.2
env_list =
lint
- py{py3, 311, 310, 39, 38}
+ py{py3, 312, 311, 310, 39, 38}
[testenv]
deps =
diff --git a/wheels/README.md b/wheels/README.md
new file mode 100644
index 000000000..c15c034b6
--- /dev/null
+++ b/wheels/README.md
@@ -0,0 +1,31 @@
+README
+------
+
+This directory creates wheels for tagged versions of Pillow.
+
+Archives
+--------
+
+https://github.com/python-pillow/pillow-depends contains archives for libraries
+that will be built as part of the Pillow build.
+
+In general, there is no need to put library archives there, because the
+`multibuild` scripts will download them from their respective URLs.
+
+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
+`multibuild/library_builders.sh` for the filename to give to the downloaded
+archive.
+
+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
+[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
new file mode 100644
index 000000000..b6f89bf72
--- /dev/null
+++ b/wheels/config.sh
@@ -0,0 +1,187 @@
+# 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.0
+OPENJPEG_VERSION=2.5.0
+XZ_VERSION=5.4.4
+TIFF_VERSION=4.5.1
+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/BROTLI.txt b/wheels/dependency_licenses/BROTLI.txt
new file mode 100644
index 000000000..33b7cdd2d
--- /dev/null
+++ b/wheels/dependency_licenses/BROTLI.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.
+
+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.
diff --git a/wheels/dependency_licenses/BZIP2.txt b/wheels/dependency_licenses/BZIP2.txt
new file mode 100644
index 000000000..d3edf477d
--- /dev/null
+++ b/wheels/dependency_licenses/BZIP2.txt
@@ -0,0 +1,42 @@
+
+--------------------------------------------------------------------------
+
+This program, "bzip2", the associated library "libbzip2", and all
+documentation, are copyright (C) 1996-2019 Julian R Seward. All
+rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. The origin of this software must not be misrepresented; you must
+ not claim that you wrote the original software. If you use this
+ software in a product, an acknowledgment in the product
+ documentation would be appreciated but is not required.
+
+3. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+4. The name of the author may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Julian Seward, jseward@acm.org
+bzip2/libbzip2 version 1.0.8 of 13 July 2019
+
+--------------------------------------------------------------------------
diff --git a/wheels/dependency_licenses/FREETYPE2.txt b/wheels/dependency_licenses/FREETYPE2.txt
new file mode 100644
index 000000000..cca8d8ce1
--- /dev/null
+++ b/wheels/dependency_licenses/FREETYPE2.txt
@@ -0,0 +1,40 @@
+The FreeType 2 font engine is copyrighted work and cannot be used
+legally without a software license. In order to make this project
+usable to a vast majority of developers, we distribute it under two
+mutually exclusive open-source licenses.
+
+This means that *you* must choose *one* of the two licenses described
+below, then obey all its terms and conditions when using FreeType 2 in
+any of your projects or products.
+
+ - The FreeType License, found in the file `docs/FTL.TXT`, which is
+ similar to the original BSD license *with* an advertising clause
+ that forces you to explicitly cite the FreeType project in your
+ product's documentation. All details are in the license file.
+ This license is suited to products which don't use the GNU General
+ Public License.
+
+ Note that this license is compatible to the GNU General Public
+ License version 3, but not version 2.
+
+ - The GNU General Public License version 2, found in
+ `docs/GPLv2.TXT` (any later version can be used also), for
+ programs which already use the GPL. Note that the FTL is
+ incompatible with GPLv2 due to its advertisement clause.
+
+The contributed BDF and PCF drivers come with a license similar to
+that of the X Window System. It is compatible to the above two
+licenses (see files `src/bdf/README` and `src/pcf/README`). The same
+holds for the source code files `src/base/fthash.c` and
+`include/freetype/internal/fthash.h`; they were part of the BDF driver
+in earlier FreeType versions.
+
+The gzip module uses the zlib license (see `src/gzip/zlib.h`) which
+too is compatible to the above two licenses.
+
+The files `src/autofit/ft-hb.c` and `src/autofit/ft-hb.h` contain code
+taken almost verbatim from the HarfBuzz file `hb-ft.cc`, which uses
+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.
diff --git a/wheels/dependency_licenses/HARFBUZZ.txt b/wheels/dependency_licenses/HARFBUZZ.txt
new file mode 100644
index 000000000..1dd917e9f
--- /dev/null
+++ b/wheels/dependency_licenses/HARFBUZZ.txt
@@ -0,0 +1,42 @@
+HarfBuzz is licensed under the so-called "Old MIT" license. Details follow.
+For parts of HarfBuzz that are licensed under different licenses see individual
+files names COPYING in subdirectories where applicable.
+
+Copyright © 2010-2022 Google, Inc.
+Copyright © 2015-2020 Ebrahim Byagowi
+Copyright © 2019,2020 Facebook, Inc.
+Copyright © 2012,2015 Mozilla Foundation
+Copyright © 2011 Codethink Limited
+Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies)
+Copyright © 2009 Keith Stribley
+Copyright © 2011 Martin Hosken and SIL International
+Copyright © 2007 Chris Wilson
+Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod
+Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc.
+Copyright © 1998-2005 David Turner and Werner Lemberg
+Copyright © 2016 Igalia S.L.
+Copyright © 2022 Matthias Clasen
+Copyright © 2018,2021 Khaled Hosny
+Copyright © 2018,2019,2020 Adobe, Inc
+Copyright © 2013-2015 Alexei Podtelezhnikov
+
+For full copyright notices consult the individual files in the package.
+
+
+Permission is hereby granted, without written agreement and without
+license or royalty fees, to use, copy, modify, and distribute this
+software and its documentation for any purpose, provided that the
+above copyright notice and the following two paragraphs appear in
+all copies of this software.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
+DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
+IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
+PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
diff --git a/wheels/dependency_licenses/LCMS2.txt b/wheels/dependency_licenses/LCMS2.txt
new file mode 100644
index 000000000..21ed6fb86
--- /dev/null
+++ b/wheels/dependency_licenses/LCMS2.txt
@@ -0,0 +1,8 @@
+Little CMS
+Copyright (c) 1998-2020 Marti Maria Saguer
+
+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.
diff --git a/wheels/dependency_licenses/LIBJPEG.txt b/wheels/dependency_licenses/LIBJPEG.txt
new file mode 100644
index 000000000..b5451f405
--- /dev/null
+++ b/wheels/dependency_licenses/LIBJPEG.txt
@@ -0,0 +1,43 @@
+1. We don't promise that this software works. (But if you find any bugs,
+ please let us know!)
+2. You can use this software for whatever you want. You don't have to pay us.
+3. You may not pretend that you wrote this software. If you use it in a
+ program, you must acknowledge somewhere in your documentation that
+ you've used the IJG code.
+
+In legalese:
+
+The authors make NO WARRANTY or representation, either express or implied,
+with respect to this software, its quality, accuracy, merchantability, or
+fitness for a particular purpose. This software is provided "AS IS", and you,
+its user, assume the entire risk as to its quality and accuracy.
+
+This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding.
+All Rights Reserved except as specified below.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+software (or portions thereof) for any purpose, without fee, subject to these
+conditions:
+(1) If any part of the source code for this software is distributed, then this
+README file must be included, with this copyright and no-warranty notice
+unaltered; and any additions, deletions, or changes to the original files
+must be clearly indicated in accompanying documentation.
+(2) If only executable code is distributed, then the accompanying
+documentation must state that "this software is based in part on the work of
+the Independent JPEG Group".
+(3) Permission for use of this software is granted only if the user accepts
+full responsibility for any undesirable consequences; the authors accept
+NO LIABILITY for damages of any kind.
+
+These conditions apply to any software derived from or based on the IJG code,
+not just to the unmodified library. If you use our work, you ought to
+acknowledge us.
+
+Permission is NOT granted for the use of any IJG author's name or company name
+in advertising or publicity relating to this software or products derived from
+it. This software may be referred to only as "the Independent JPEG Group's
+software".
+
+We specifically permit and encourage the use of this software as the basis of
+commercial products, provided that all warranty or liability claims are
+assumed by the product vendor.
diff --git a/wheels/dependency_licenses/LIBLZMA.txt b/wheels/dependency_licenses/LIBLZMA.txt
new file mode 100644
index 000000000..43c7a23ba
--- /dev/null
+++ b/wheels/dependency_licenses/LIBLZMA.txt
@@ -0,0 +1,63 @@
+XZ Utils Licensing
+==================
+
+ Different licenses apply to different files in this package. Here
+ is a rough summary of which licenses apply to which parts of this
+ package (but check the individual files to be sure!):
+
+ - liblzma is in the public domain.
+
+ - xz, xzdec, and lzmadec command line tools are in the public
+ domain unless GNU getopt_long had to be compiled and linked
+ in from the lib directory. The getopt_long code is under
+ GNU LGPLv2.1+.
+
+ - The scripts to grep, diff, and view compressed files have been
+ adapted from gzip. These scripts and their documentation are
+ under GNU GPLv2+.
+
+ - All the documentation in the doc directory and most of the
+ XZ Utils specific documentation files in other directories
+ are in the public domain.
+
+ - Translated messages are in the public domain.
+
+ - The build system contains public domain files, and files that
+ are under GNU GPLv2+ or GNU GPLv3+. None of these files end up
+ in the binaries being built.
+
+ - Test files and test code in the tests directory, and debugging
+ utilities in the debug directory are in the public domain.
+
+ - The extra directory may contain public domain files, and files
+ that are under various free software licenses.
+
+ You can do whatever you want with the files that have been put into
+ the public domain. If you find public domain legally problematic,
+ take the previous sentence as a license grant. If you still find
+ the lack of copyright legally problematic, you have too many
+ lawyers.
+
+ As usual, this software is provided "as is", without any warranty.
+
+ If you copy significant amounts of public domain code from XZ Utils
+ into your project, acknowledging this somewhere in your software is
+ polite (especially if it is proprietary, non-free software), but
+ naturally it is not legally required. Here is an example of a good
+ notice to put into "about box" or into documentation:
+
+ This software includes code from XZ Utils .
+
+ The following license texts are included in the following files:
+ - COPYING.LGPLv2.1: GNU Lesser General Public License version 2.1
+ - COPYING.GPLv2: GNU General Public License version 2
+ - COPYING.GPLv3: GNU General Public License version 3
+
+ Note that the toolchain (compiler, linker etc.) may add some code
+ pieces that are copyrighted. Thus, it is possible that e.g. liblzma
+ binary wouldn't actually be in the public domain in its entirety
+ even though it contains no copyrighted code from the XZ Utils source
+ package.
+
+ If you have questions, don't hesitate to ask the author(s) for more
+ information.
diff --git a/wheels/dependency_licenses/LIBPNG.txt b/wheels/dependency_licenses/LIBPNG.txt
new file mode 100644
index 000000000..c8ad24eec
--- /dev/null
+++ b/wheels/dependency_licenses/LIBPNG.txt
@@ -0,0 +1,134 @@
+COPYRIGHT NOTICE, DISCLAIMER, and LICENSE
+=========================================
+
+PNG Reference Library License version 2
+---------------------------------------
+
+ * Copyright (c) 1995-2022 The PNG Reference Library Authors.
+ * Copyright (c) 2018-2022 Cosmin Truta.
+ * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.
+ * Copyright (c) 1996-1997 Andreas Dilger.
+ * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+The software is supplied "as is", without warranty of any kind,
+express or implied, including, without limitation, the warranties
+of merchantability, fitness for a particular purpose, title, and
+non-infringement. In no event shall the Copyright owners, or
+anyone distributing the software, be liable for any damages or
+other liability, whether in contract, tort or otherwise, arising
+from, out of, or in connection with the software, or the use or
+other dealings in the software, even if advised of the possibility
+of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute
+this software, or portions hereof, for any purpose, without fee,
+subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you
+ must not claim that you wrote the original software. If you
+ use this software in a product, an acknowledgment in the product
+ documentation would be appreciated, but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+ not be misrepresented as being the original software.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+
+PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)
+-----------------------------------------------------------------------
+
+libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are
+Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are
+derived from libpng-1.0.6, and are distributed according to the same
+disclaimer and license as libpng-1.0.6 with the following individuals
+added to the list of Contributing Authors:
+
+ Simon-Pierre Cadieux
+ Eric S. Raymond
+ Mans Rullgard
+ Cosmin Truta
+ Gilles Vollant
+ James Yu
+ Mandar Sahastrabuddhe
+ Google Inc.
+ Vadim Barkov
+
+and with the following additions to the disclaimer:
+
+ There is no warranty against interference with your enjoyment of
+ the library or against infringement. There is no warranty that our
+ efforts or the library will fulfill any of your particular purposes
+ or needs. This library is provided with all faults, and the entire
+ risk of satisfactory quality, performance, accuracy, and effort is
+ with the user.
+
+Some files in the "contrib" directory and some configure-generated
+files that are distributed with libpng have other copyright owners, and
+are released under other open source licenses.
+
+libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are
+Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from
+libpng-0.96, and are distributed according to the same disclaimer and
+license as libpng-0.96, with the following individuals added to the
+list of Contributing Authors:
+
+ Tom Lane
+ Glenn Randers-Pehrson
+ Willem van Schaik
+
+libpng versions 0.89, June 1996, through 0.96, May 1997, are
+Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,
+and are distributed according to the same disclaimer and license as
+libpng-0.88, with the following individuals added to the list of
+Contributing Authors:
+
+ John Bowler
+ Kevin Bracey
+ Sam Bushell
+ Magnus Holmgren
+ Greg Roelofs
+ Tom Tanner
+
+Some files in the "scripts" directory have other copyright owners,
+but are released under this license.
+
+libpng versions 0.5, May 1995, through 0.88, January 1996, are
+Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.
+
+For the purposes of this copyright and license, "Contributing Authors"
+is defined as the following set of individuals:
+
+ Andreas Dilger
+ Dave Martindale
+ Guy Eric Schalnat
+ Paul Schmidt
+ Tim Wegner
+
+The PNG Reference Library is supplied "AS IS". The Contributing
+Authors and Group 42, Inc. disclaim all warranties, expressed or
+implied, including, without limitation, the warranties of
+merchantability and of fitness for any purpose. The Contributing
+Authors and Group 42, Inc. assume no liability for direct, indirect,
+incidental, special, exemplary, or consequential damages, which may
+result from the use of the PNG Reference Library, even if advised of
+the possibility of such damage.
+
+Permission is hereby granted to use, copy, modify, and distribute this
+source code, or portions hereof, for any purpose, without fee, subject
+to the following restrictions:
+
+ 1. The origin of this source code must not be misrepresented.
+
+ 2. Altered versions must be plainly marked as such and must not
+ be misrepresented as being the original source.
+
+ 3. This Copyright notice may not be removed or altered from any
+ source or altered source distribution.
+
+The Contributing Authors and Group 42, Inc. specifically permit,
+without fee, and encourage the use of this source code as a component
+to supporting the PNG file format in commercial products. If you use
+this source code in a product, acknowledgment is not required but would
+be appreciated.
diff --git a/wheels/dependency_licenses/LIBTIFF.txt b/wheels/dependency_licenses/LIBTIFF.txt
new file mode 100644
index 000000000..dc255dec6
--- /dev/null
+++ b/wheels/dependency_licenses/LIBTIFF.txt
@@ -0,0 +1,21 @@
+Copyright (c) 1988-1997 Sam Leffler
+Copyright (c) 1991-1997 Silicon Graphics, Inc.
+
+Permission to use, copy, modify, distribute, and sell this software and
+its documentation for any purpose is hereby granted without fee, provided
+that (i) the above copyright notices and this permission notice appear in
+all copies of the software and related documentation, and (ii) the names of
+Sam Leffler and Silicon Graphics may not be used in any advertising or
+publicity relating to the software without the specific, prior written
+permission of Sam Leffler and Silicon Graphics.
+
+THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
+
+IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
+ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
+LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+OF THIS SOFTWARE.
diff --git a/wheels/dependency_licenses/LIBWEBP.txt b/wheels/dependency_licenses/LIBWEBP.txt
new file mode 100644
index 000000000..83e4e6f6d
--- /dev/null
+++ b/wheels/dependency_licenses/LIBWEBP.txt
@@ -0,0 +1,29 @@
+Copyright (c) 2010, Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/wheels/dependency_licenses/OPENJPEG.txt b/wheels/dependency_licenses/OPENJPEG.txt
new file mode 100644
index 000000000..c41fc21d8
--- /dev/null
+++ b/wheels/dependency_licenses/OPENJPEG.txt
@@ -0,0 +1,39 @@
+*
+ * The copyright in this software is being made available under the 2-clauses
+ * BSD License, included below. This software may be subject to other third
+ * party and contributor rights, including patent rights, and no such rights
+ * are granted under this license.
+ *
+ * Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium
+ * Copyright (c) 2002-2014, Professor Benoit Macq
+ * Copyright (c) 2003-2014, Antonin Descampe
+ * Copyright (c) 2003-2009, Francois-Olivier Devaux
+ * Copyright (c) 2005, Herve Drolon, FreeImage Team
+ * Copyright (c) 2002-2003, Yannick Verschueren
+ * Copyright (c) 2001-2003, David Janssens
+ * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France
+ * Copyright (c) 2012, CS Systemes d'Information, France
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS'
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
diff --git a/wheels/dependency_licenses/RAQM.txt b/wheels/dependency_licenses/RAQM.txt
new file mode 100644
index 000000000..196511ef6
--- /dev/null
+++ b/wheels/dependency_licenses/RAQM.txt
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright © 2015 Information Technology Authority (ITA)
+Copyright © 2016 Khaled Hosny
+
+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.
diff --git a/wheels/dependency_licenses/XAU.txt b/wheels/dependency_licenses/XAU.txt
new file mode 100644
index 000000000..64492ad80
--- /dev/null
+++ b/wheels/dependency_licenses/XAU.txt
@@ -0,0 +1,21 @@
+Copyright 1988, 1993, 1994, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+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
+OPEN GROUP 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.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
diff --git a/wheels/dependency_licenses/XCB.txt b/wheels/dependency_licenses/XCB.txt
new file mode 100644
index 000000000..54bfbe5b0
--- /dev/null
+++ b/wheels/dependency_licenses/XCB.txt
@@ -0,0 +1,30 @@
+Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett.
+All Rights Reserved.
+
+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
+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.
+
+Except as contained in this notice, the names of the authors
+or their institutions shall not be used in advertising or
+otherwise to promote the sale, use or other dealings in this
+Software without prior written authorization from the
+authors.
diff --git a/wheels/dependency_licenses/XDMCP.txt b/wheels/dependency_licenses/XDMCP.txt
new file mode 100644
index 000000000..5532d143c
--- /dev/null
+++ b/wheels/dependency_licenses/XDMCP.txt
@@ -0,0 +1,23 @@
+Copyright 1989, 1998 The Open Group
+
+Permission to use, copy, modify, distribute, and sell this software and its
+documentation for any purpose is hereby granted without fee, provided that
+the above copyright notice appear in all copies and that both that
+copyright notice and this permission notice appear in supporting
+documentation.
+
+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
+OPEN GROUP 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.
+
+Except as contained in this notice, the name of The Open Group shall not be
+used in advertising or otherwise to promote the sale, use or other dealings
+in this Software without prior written authorization from The Open Group.
+
+Author: Keith Packard, MIT X Consortium
diff --git a/wheels/dependency_licenses/ZLIB.txt b/wheels/dependency_licenses/ZLIB.txt
new file mode 100644
index 000000000..84def6dc6
--- /dev/null
+++ b/wheels/dependency_licenses/ZLIB.txt
@@ -0,0 +1,29 @@
+ (C) 1995-2017 Jean-loup Gailly and Mark Adler
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+ Jean-loup Gailly Mark Adler
+ jloup@gzip.org madler@alumni.caltech.edu
+
+If you use the zlib library in a product, we would appreciate *not* receiving
+lengthy legal documents to sign. The sources are provided for free but without
+warranty of any kind. The library has been entirely written by Jean-loup
+Gailly and Mark Adler; it does not include third-party code.
+
+If you redistribute modified sources, we would appreciate that you include in
+the file ChangeLog history information documenting your changes. Please read
+the FAQ for more information on the distribution of modified source versions.
diff --git a/wheels/multibuild b/wheels/multibuild
new file mode 160000
index 000000000..452dd2d17
--- /dev/null
+++ b/wheels/multibuild
@@ -0,0 +1 @@
+Subproject commit 452dd2d1705f6b2375369a6570c415beb3163f70
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index df834a387..3b85ff8d7 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import argparse
import os
import platform
@@ -7,42 +9,41 @@ import struct
import subprocess
-def cmd_cd(path):
+def cmd_cd(path: str) -> str:
return f"cd /D {path}"
-def cmd_set(name, value):
+def cmd_set(name: str, value: str) -> str:
return f"set {name}={value}"
-def cmd_append(name, value):
+def cmd_append(name: str, value: str) -> str:
op = "path " if name == "PATH" else f"set {name}="
return op + f"%{name}%;{value}"
-def cmd_copy(src, tgt):
+def cmd_copy(src: str, tgt: str) -> str:
return f'copy /Y /B "{src}" "{tgt}"'
-def cmd_xcopy(src, tgt):
+def cmd_xcopy(src: str, tgt: str) -> str:
return f'xcopy /Y /E "{src}" "{tgt}"'
-def cmd_mkdir(path):
+def cmd_mkdir(path: str) -> str:
return f'mkdir "{path}"'
-def cmd_rmdir(path):
+def cmd_rmdir(path: str) -> str:
return f'rmdir /S /Q "{path}"'
-def cmd_nmake(makefile=None, target="", params=None):
- if params is None:
- params = ""
- elif isinstance(params, (list, tuple)):
- params = " ".join(params)
- else:
- params = str(params)
+def cmd_nmake(
+ makefile: str | None = None,
+ target: str = "",
+ params: list[str] | None = None,
+) -> str:
+ params = "" if params is None else " ".join(params)
return " ".join(
[
@@ -55,7 +56,7 @@ def cmd_nmake(makefile=None, target="", params=None):
)
-def cmds_cmake(target, *params):
+def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]:
if not isinstance(target, str):
target = " ".join(target)
@@ -80,8 +81,11 @@ def cmds_cmake(target, *params):
def cmd_msbuild(
- file, configuration="Release", target="Build", platform="{msbuild_arch}"
-):
+ file: str,
+ configuration: str = "Release",
+ target: str = "Build",
+ platform: str = "{msbuild_arch}",
+) -> str:
return " ".join(
[
"{msbuild}",
@@ -96,14 +100,14 @@ def cmd_msbuild(
SF_PROJECTS = "https://sourceforge.net/projects"
-architectures = {
+ARCHITECTURES = {
"x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"},
"x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"},
"ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"},
}
# dependencies, listed in order of compilation
-deps = {
+DEPS = {
"libjpeg": {
"url": SF_PROJECTS
+ "/libjpeg-turbo/files/3.0.0/libjpeg-turbo-3.0.0.tar.gz/download",
@@ -130,9 +134,9 @@ deps = {
"bins": ["cjpeg.exe", "djpeg.exe"],
},
"zlib": {
- "url": "https://zlib.net/zlib1213.zip",
- "filename": "zlib1213.zip",
- "dir": "zlib-1.2.13",
+ "url": "https://zlib.net/zlib13.zip",
+ "filename": "zlib13.zip",
+ "dir": "zlib-1.3",
"license": "README",
"license_pattern": "Copyright notice:\n\n(.+)$",
"build": [
@@ -144,9 +148,9 @@ deps = {
"libs": [r"*.lib"],
},
"xz": {
- "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download",
- "filename": "xz-5.4.3.tar.gz",
- "dir": "xz-5.4.3",
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.4.tar.gz/download",
+ "filename": "xz-5.4.4.tar.gz",
+ "dir": "xz-5.4.4",
"license": "COPYING",
"build": [
*cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"),
@@ -157,9 +161,9 @@ deps = {
"libs": [r"liblzma.lib"],
},
"libwebp": {
- "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.1.tar.gz",
- "filename": "libwebp-1.3.1.tar.gz",
- "dir": "libwebp-1.3.1",
+ "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.2.tar.gz",
+ "filename": "libwebp-1.3.2.tar.gz",
+ "dir": "libwebp-1.3.2",
"license": "COPYING",
"build": [
cmd_rmdir(r"output\release-static"), # clean
@@ -235,9 +239,9 @@ deps = {
"libs": ["*.lib"],
},
"freetype": {
- "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.1.tar.gz", # noqa: E501
- "filename": "freetype-2.13.1.tar.gz",
- "dir": "freetype-2.13.1",
+ "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz", # noqa: E501
+ "filename": "freetype-2.13.2.tar.gz",
+ "dir": "freetype-2.13.2",
"license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
"patch": {
r"builds\windows\vc2010\freetype.vcxproj": {
@@ -335,9 +339,9 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/8.0.0.zip",
- "filename": "harfbuzz-8.0.0.zip",
- "dir": "harfbuzz-8.0.0",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/8.2.1.zip",
+ "filename": "harfbuzz-8.2.1.zip",
+ "dir": "harfbuzz-8.2.1",
"license": "COPYING",
"build": [
*cmds_cmake(
@@ -365,7 +369,7 @@ deps = {
# based on distutils._msvccompiler from CPython 3.7.4
-def find_msvs():
+def find_msvs() -> dict[str, str] | None:
root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
if not root:
print("Program Files not found")
@@ -421,25 +425,40 @@ def find_msvs():
}
-def extract_dep(url, filename):
- import tarfile
+def download_dep(url: str, file: str) -> None:
import urllib.request
+
+ ex = None
+ for i in range(3):
+ try:
+ print(f"Fetching {url} (attempt {i + 1})...")
+ content = urllib.request.urlopen(url).read()
+ with open(file, "wb") as f:
+ f.write(content)
+ break
+ except urllib.error.URLError as e:
+ ex = e
+ else:
+ raise RuntimeError(ex)
+
+
+def extract_dep(url: str, filename: str) -> None:
+ import tarfile
import zipfile
file = os.path.join(args.depends_dir, filename)
if not os.path.exists(file):
- ex = None
- for i in range(3):
- try:
- print("Fetching %s (attempt %d)..." % (url, i + 1))
- content = urllib.request.urlopen(url).read()
- with open(file, "wb") as f:
- f.write(content)
- break
- except urllib.error.URLError as e:
- ex = e
- else:
- raise RuntimeError(ex)
+ # First try our mirror
+ mirror_url = (
+ f"https://raw.githubusercontent.com/"
+ f"python-pillow/pillow-depends/main/{filename}"
+ )
+ try:
+ download_dep(mirror_url, file)
+ except RuntimeError as exc:
+ # Otherwise try upstream
+ print(exc)
+ download_dep(url, file)
print("Extracting " + filename)
sources_dir_abs = os.path.abspath(sources_dir)
@@ -466,7 +485,7 @@ def extract_dep(url, filename):
raise RuntimeError(msg)
-def write_script(name, lines):
+def write_script(name: str, lines: list[str]) -> None:
name = os.path.join(args.build_dir, name)
lines = [line.format(**prefs) for line in lines]
print("Writing " + name)
@@ -477,7 +496,7 @@ def write_script(name, lines):
print(" " + line)
-def get_footer(dep):
+def get_footer(dep: dict) -> list[str]:
lines = []
for out in dep.get("headers", []):
lines.append(cmd_copy(out, "{inc_dir}"))
@@ -488,7 +507,7 @@ def get_footer(dep):
return lines
-def build_env():
+def build_env() -> None:
lines = [
"if defined DISTUTILS_USE_SDK goto end",
cmd_set("INCLUDE", "{inc_dir}"),
@@ -504,8 +523,8 @@ def build_env():
write_script("build_env.cmd", lines)
-def build_dep(name):
- dep = deps[name]
+def build_dep(name: str) -> str:
+ dep = DEPS[name]
dir = dep["dir"]
file = f"build_dep_{name}.cmd"
@@ -554,9 +573,9 @@ def build_dep(name):
return file
-def build_dep_all():
+def build_dep_all() -> None:
lines = [r'call "{build_dir}\build_env.cmd"']
- for dep_name in deps:
+ for dep_name in DEPS:
print()
if dep_name in disabled:
print(f"Skipping disabled dependency {dep_name}")
@@ -602,7 +621,7 @@ if __name__ == "__main__":
)
parser.add_argument(
"--architecture",
- choices=architectures,
+ choices=ARCHITECTURES,
default=os.environ.get(
"ARCHITECTURE",
(
@@ -634,7 +653,7 @@ if __name__ == "__main__":
)
args = parser.parse_args()
- arch_prefs = architectures[args.architecture]
+ arch_prefs = ARCHITECTURES[args.architecture]
print("Target architecture:", args.architecture)
msvs = find_msvs()
@@ -693,7 +712,7 @@ if __name__ == "__main__":
# TODO find NASM automatically
}
- for k, v in deps.items():
+ for k, v in DEPS.items():
prefs[f"dir_{k}"] = os.path.join(sources_dir, v["dir"])
print()