Merge branch 'main' into windows1co

This commit is contained in:
Andrew Murray 2023-11-14 22:07:48 +11:00
commit 41d7a3757d
185 changed files with 3695 additions and 849 deletions

View File

@ -10,7 +10,7 @@ environment:
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES
matrix: matrix:
- PYTHON: C:/Python311 - PYTHON: C:/Python312
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64 - PYTHON: C:/Python38-x64
@ -43,7 +43,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout' - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

@ -28,7 +28,8 @@ fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
PYTHONOPTIMIZE=0 python3 -m pip install cffi # TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
@ -38,7 +39,8 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy # TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@ -46,6 +48,16 @@ if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install pyqt6 python3 -m pip install pyqt6
fi fi
# Pyroma uses non-isolated build and fails with old setuptools
if [[
$GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.8
|| $GHA_PYTHON_VERSION == 3.9
]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=67.8"
fi
# webp # webp
pushd depends && ./install_webp.sh && popd pushd depends && ./install_webp.sh && popd

View File

@ -13,7 +13,7 @@ indent_style = space
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.yml] [*.{toml,yml}]
# Two-space indentation # Two-space indentation
indent_size = 2 indent_size = 2

View File

@ -2,6 +2,8 @@ name: CIFuzz
on: on:
push: push:
branches:
- "**"
paths: paths:
- ".github/workflows/cifuzz.yml" - ".github/workflows/cifuzz.yml"
- "**.c" - "**.c"

View File

@ -2,6 +2,8 @@ name: Docs
on: on:
push: push:
branches:
- "**"
paths: paths:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- "docs/**" - "docs/**"

View File

@ -5,7 +5,9 @@ set -e
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
PYTHONOPTIMIZE=0 python3 -m pip install cffi # TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
@ -14,7 +16,8 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install numpy # TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -2,13 +2,23 @@ name: Test Cygwin
on: on:
push: push:
branches:
- "**"
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:

View File

@ -2,13 +2,23 @@ name: Test Docker
on: on:
push: push:
branches:
- "**"
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -41,8 +51,8 @@ jobs:
debian-11-bullseye-amd64, debian-11-bullseye-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-37-amd64,
fedora-38-amd64, fedora-38-amd64,
fedora-39-amd64,
gentoo, gentoo,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,

View File

@ -2,13 +2,23 @@ name: Test MinGW
on: on:
push: push:
branches:
- "**"
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:

View File

@ -1,9 +1,11 @@
name: Test Valgrind name: Test Valgrind
# like the docker tests, but running valgrind only on *.c/*.h changes. # like the Docker tests, but running valgrind only on *.c/*.h changes.
on: on:
push: push:
branches:
- "**"
paths: paths:
- ".github/workflows/test-valgrind.yml" - ".github/workflows/test-valgrind.yml"
- "**.c" - "**.c"

View File

@ -4,11 +4,19 @@ on:
push: push:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -24,7 +32,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: 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", "3.13"]
timeout-minutes: 30 timeout-minutes: 30
@ -51,14 +59,15 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml" cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information - name: Print build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
- name: Install dependencies - name: Install dependencies
id: install id: install

View File

@ -2,13 +2,23 @@ name: Test
on: on:
push: push:
branches:
- "**"
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
pull_request: pull_request:
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*"
- ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -31,7 +41,8 @@ jobs:
python-version: [ python-version: [
"pypy3.10", "pypy3.10",
"pypy3.9", "pypy3.9",
"3.12-dev", "3.13",
"3.12",
"3.11", "3.11",
"3.10", "3.10",
"3.9", "3.9",
@ -54,6 +65,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip cache: pip
cache-dependency-path: ".ci/*.sh" cache-dependency-path: ".ci/*.sh"

40
.github/workflows/wheels-build.sh vendored Executable file
View File

@ -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

69
.github/workflows/wheels-linux.yml vendored Normal file
View File

@ -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

57
.github/workflows/wheels-macos.yml vendored Normal file
View File

@ -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

64
.github/workflows/wheels.yml vendored Normal file
View File

@ -0,0 +1,64 @@
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
env:
FORCE_COLOR: 1
jobs:
macos:
uses: ./.github/workflows/wheels-macos.yml
with:
artifacts-name: "dist"
linux:
uses: ./.github/workflows/wheels-linux.yml
with:
artifacts-name: "dist"
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
cache: pip
cache-dependency-path: "Makefile"
- run: make sdist
- uses: actions/upload-artifact@v3
with:
name: dist
path: dist/*.tar.gz
success:
permissions:
contents: none
needs: [macos, linux, sdist]
runs-on: ubuntu-latest
name: Wheels Successful
steps:
- name: Success
run: echo Wheels Successful

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "multibuild"]
path = wheels/multibuild
url = https://github.com/multi-build/multibuild.git

View File

@ -1,20 +1,14 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v3.13.0 rev: v0.1.4
hooks: hooks:
- id: pyupgrade - id: ruff
args: [--py38-plus] args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.9.1 rev: 23.10.1
hooks: hooks:
- id: black - id: black
args: [--target-version=py38]
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.5 rev: 1.7.5
@ -23,32 +17,19 @@ repos:
args: [--severity-level=high] args: [--severity-level=high]
files: ^src/ files: ^src/
- repo: https://github.com/asottile/yesqa
rev: v1.5.0
hooks:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4 rev: v1.5.4
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies:
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0 rev: v1.10.0
hooks: hooks:
- id: python-check-blanket-noqa
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-merge-conflict - id: check-merge-conflict
@ -61,17 +42,17 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6.8 rev: v0.8.1
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.1.0 rev: 1.4.1
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.14 rev: v0.15
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

135
.travis.yml Normal file
View File

@ -0,0 +1,135 @@
if: tag IS present OR type = api
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.12"
dist: jammy
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

View File

@ -2,9 +2,75 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
10.1.0 (unreleased) 10.2.0 (unreleased)
------------------- -------------------
- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491
[bgilbert, radarhere]
- If save_all PNG only has one frame, do not create animated image #7522
[radarhere]
- Fixed frombytes() for images with a zero dimension #7493
[radarhere]
10.1.0 (2023-10-15)
-------------------
- Added TrueType default font to allow for different sizes #7354
[radarhere]
- Fixed invalid argument warning #7442
[radarhere]
- Added ImageOps cover method #7412
[radarhere, hugovk]
- Catch struct.error from truncated EXIF when reading JPEG DPI #7458
[radarhere]
- Consider default image when selecting mode for PNG save_all #7437
[radarhere]
- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303
[radarhere]
- Added CMYK to RGB unpacker #7310
[radarhere]
- Improved flexibility of XMP parsing #7274
[radarhere]
- Support reading 8-bit YCbCr TIFF images #7415
[radarhere]
- 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 - Fixed bug when reading BC5S DDS images #7401
[radarhere] [radarhere]
@ -2137,7 +2203,7 @@ Changelog (Pillow)
- Cache EXIF information #3498 - Cache EXIF information #3498
[Glandos] [Glandos]
- Added transparency for all PNG greyscale modes #3744 - Added transparency for all PNG grayscale modes #3744
[radarhere] [radarhere]
- Fix deprecation warnings in Python 3.8 #3749 - Fix deprecation warnings in Python 3.8 #3749
@ -4639,7 +4705,7 @@ Changelog (Pillow)
- Fix Bicubic interpolation #970 - Fix Bicubic interpolation #970
[homm] [homm]
- Support for 4-bit greyscale TIFF images #980 - Support for 4-bit grayscale TIFF images #980
[hugovk] [hugovk]
- Updated manifest #957 - Updated manifest #957
@ -6714,7 +6780,7 @@ The test suite includes 750 individual tests.
- You can now convert directly between all modes supported by - You can now convert directly between all modes supported by
PIL. When converting colour images to "P", PIL defaults to PIL. When converting colour images to "P", PIL defaults to
a "web" palette and dithering. When converting greyscale a "web" palette and dithering. When converting grayscale
images to "1", PIL uses a thresholding and dithering. images to "1", PIL uses a thresholding and dithering.
- Added a "dither" option to "convert". By default, "convert" - Added a "dither" option to "convert". By default, "convert"
@ -6792,13 +6858,13 @@ The test suite includes 530 individual tests.
- Fixed "paste" to allow a mask also for mode "F" images. - Fixed "paste" to allow a mask also for mode "F" images.
- The BMP driver now saves mode "1" images. When loading images, the mode - The BMP driver now saves mode "1" images. When loading images, the mode
is set to "L" for 8-bit files with greyscale palettes, and to "P" for is set to "L" for 8-bit files with grayscale palettes, and to "P" for
other 8-bit files. other 8-bit files.
- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1"). - The IM driver now reads and saves "1" images (file modes "0 1" or "L 1").
- The JPEG and GIF drivers now saves "1" images. For JPEG, the image - The JPEG and GIF drivers now saves "1" images. For JPEG, the image
is saved as 8-bit greyscale (it will load as mode "L"). For GIF, the is saved as 8-bit grayscale (it will load as mode "L"). For GIF, the
image will be loaded as a "P" image. image will be loaded as a "P" image.
- Fixed a potential buffer overrun in the GIF encoder. - Fixed a potential buffer overrun in the GIF encoder.
@ -7102,7 +7168,7 @@ The test suite includes 400 individual tests.
drawing capabilities can be used to render vector and metafile drawing capabilities can be used to render vector and metafile
formats. formats.
- Added restricted drivers for images from Image Tools (greyscale - Added restricted drivers for images from Image Tools (grayscale
only) and LabEye/IFUNC (common interchange modes only). only) and LabEye/IFUNC (common interchange modes only).
- Some minor improvements to the sample scripts provided in the - Some minor improvements to the sample scripts provided in the

View File

@ -5,8 +5,10 @@ include *.md
include *.py include *.py
include *.rst include *.rst
include *.sh include *.sh
include *.toml
include *.txt include *.txt
include *.yaml include *.yaml
include .flake8
include LICENSE include LICENSE
include Makefile include Makefile
include tox.ini include tox.ini
@ -29,3 +31,4 @@ global-exclude .git*
global-exclude *.pyc global-exclude *.pyc
global-exclude *.so global-exclude *.so
prune .ci prune .ci
prune wheels

View File

@ -49,7 +49,7 @@ help:
@echo " install make and install" @echo " install make and install"
@echo " install-coverage make and install with C coverage" @echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks" @echo " lint run the lint checks"
@echo " lint-fix run Black and isort to (mostly) fix lint issues" @echo " lint-fix run Ruff to (mostly) fix lint issues"
@echo " release-test run code and package tests before release" @echo " release-test run code and package tests before release"
@echo " test run tests on installed Pillow" @echo " test run tests on installed Pillow"
@ -118,6 +118,6 @@ lint:
.PHONY: lint-fix .PHONY: lint-fix
lint-fix: lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort python3 -m black .
python3 -m black --target-version py38 . python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m isort . python3 -m ruff --fix .

View File

@ -45,12 +45,12 @@ As of 2019, Pillow development is
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img <a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
alt="AppVeyor CI build status (Windows)" alt="AppVeyor CI build status (Windows)"
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a> src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
<a href="https://github.com/python-pillow/pillow-wheels/actions"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions wheels build status (Wheels)" alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
<a href="https://app.travis-ci.com/github/python-pillow/pillow-wheels"><img <a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
alt="Travis CI wheels build status (aarch64)" alt="Travis CI wheels build status (aarch64)"
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels"></a> src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a> src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
@ -74,9 +74,9 @@ As of 2019, Pillow development is
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/Pillow/"><img
alt="Number of PyPI downloads" alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a> src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img <a href="https://www.bestpractices.dev/projects/6331"><img
alt="OpenSSF Best Practices" alt="OpenSSF Best Practices"
src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a> src="https://www.bestpractices.dev/projects/6331/badge"></a>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -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 * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch. * [ ] 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 [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` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`. * [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@ -20,12 +20,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0 git tag 5.2.0
git push --tags git push --tags
``` ```
* [ ] Create and check source distribution: * [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
```bash * [ ] Check and upload all source and binary distributions e.g.:
make sdist
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
```bash ```bash
python3 -m twine check --strict dist/* python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0* python3 -m twine upload dist/Pillow-5.2.0*
@ -59,8 +55,8 @@ Released as needed for security, installation or critical bug fixes.
```bash ```bash
make sdist make sdist
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.: * [ ] Check and upload all source and binary distributions e.g.:
```bash ```bash
python3 -m twine check --strict dist/* python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.1* python3 -m twine upload dist/Pillow-5.2.1*
@ -90,26 +86,23 @@ Released as needed privately to individual vendors for critical security-related
```bash ```bash
make sdist make sdist
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push origin 2.5.x git push origin 2.5.x
``` ```
## Binary Distributions ## Source and Binary Distributions
### macOS and Linux ### macOS and Linux
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): * [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash ```bash
git clone https://github.com/python-pillow/pillow-wheels gh run download --dir dist
cd pillow-wheels # select dist
./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
``` ```
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`.
### Windows ### Windows
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)

View File

@ -45,7 +45,7 @@ def test_direct():
assert caccess[(0, 0)] == access[(0, 0)] assert caccess[(0, 0)] == access[(0, 0)]
print("Size: %sx%s" % im.size) print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access) timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access) timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess) timer(iterate_get, "C-api - get", im.size, caccess)

View File

@ -95,7 +95,7 @@ def assert_image_equal(a, b, msg=None):
except Exception: except Exception:
pass pass
assert False, msg or "got different content" pytest.fail(msg or "got different content")
def assert_image_equal_tofile(a, filename, msg=None, mode=None): def assert_image_equal_tofile(a, filename, msg=None, mode=None):

View File

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 331 B

View File

Before

Width:  |  Height:  |  Size: 668 B

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 180 B

BIN
Tests/images/palette.dds Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

BIN
Tests/images/xmp_padded.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,7 @@
# #
################################################################################ ################################################################################
python3 setup.py build --build-base=/tmp/build install python3 -m pip install .
# Build fuzzers in $OUT. # Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do for fuzzer in $(find $SRC -name 'fuzz_*.py'); do

View File

@ -231,13 +231,13 @@ def test_apng_mode():
assert im.getpixel((0, 0)) == (0, 0, 128, 191) assert im.getpixel((0, 0)) == (0, 0, 128, 191)
assert im.getpixel((64, 32)) == (0, 0, 128, 191) assert im.getpixel((64, 32)) == (0, 0, 128, 191)
with Image.open("Tests/images/apng/mode_greyscale.png") as im: with Image.open("Tests/images/apng/mode_grayscale.png") as im:
assert im.mode == "L" assert im.mode == "L"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == 128 assert im.getpixel((0, 0)) == 128
assert im.getpixel((64, 32)) == 255 assert im.getpixel((64, 32)) == 255
with Image.open("Tests/images/apng/mode_greyscale_alpha.png") as im: with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
assert im.mode == "LA" assert im.mode == "LA"
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (128, 191) assert im.getpixel((0, 0)) == (128, 191)
@ -350,7 +350,7 @@ def test_apng_save(tmp_path):
im.load() im.load()
assert not im.is_animated assert not im.is_animated
assert im.n_frames == 1 assert im.n_frames == 1
assert im.get_format_mimetype() == "image/apng" assert im.get_format_mimetype() == "image/png"
assert im.info.get("default_image") is None assert im.info.get("default_image") is None
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@ -450,26 +450,29 @@ def test_apng_save_duration_loop(tmp_path):
test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.load()
assert im.n_frames == 1 assert im.n_frames == 1
assert im.info.get("duration") == 750 assert "duration" not in im.info
different_frame = Image.new("RGBA", (128, 64))
frame.save(
test_file,
save_all=True,
append_images=[frame, different_frame],
duration=[500, 100, 150],
)
with Image.open(test_file) as im:
assert im.n_frames == 2
assert im.info["duration"] == 600
im.seek(1)
assert im.info["duration"] == 150
# test info duration # test info duration
frame.info["duration"] = 750 frame.info["duration"] = 300
frame.save(test_file, save_all=True) frame.save(test_file, save_all=True, append_images=[frame, different_frame])
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.info.get("duration") == 750 assert im.n_frames == 2
assert im.info["duration"] == 600
def test_apng_save_duplicate_duration(tmp_path):
test_file = str(tmp_path / "temp.png")
frame = Image.new("RGB", (1, 1))
# Test a single duration is correctly combined across duplicate frames
frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
with Image.open(test_file) as im:
assert im.n_frames == 1
assert im.info.get("duration") == 1500
def test_apng_save_disposal(tmp_path): def test_apng_save_disposal(tmp_path):
@ -673,10 +676,17 @@ def test_seek_after_close():
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) @pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
def test_different_modes_in_later_frames(mode, tmp_path): @pytest.mark.parametrize("default_image", (True, False))
@pytest.mark.parametrize("duplicate", (True, False))
def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) im.save(
test_file,
save_all=True,
default_image=default_image,
append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
assert reloaded.mode == mode assert reloaded.mode == mode

View File

@ -159,7 +159,7 @@ def test_rle8():
with Image.open("Tests/images/hopper_rle8.bmp") as im: with Image.open("Tests/images/hopper_rle8.bmp") as im:
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im: with Image.open("Tests/images/hopper_rle8_grayscale.bmp") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
# This test image has been manually hexedited # This test image has been manually hexedited

View File

@ -298,6 +298,11 @@ def test_dxt5_colorblock_alpha_issue_4142():
assert px[2] != 0 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(): def test_unimplemented_pixel_format():
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_pixel_format.dds"): with Image.open("Tests/images/unimplemented_pixel_format.dds"):

View File

@ -8,6 +8,7 @@ from .helper import (
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
hopper, hopper,
is_win32,
mark_if_feature_version, mark_if_feature_version,
skip_unless_feature, skip_unless_feature,
) )
@ -98,6 +99,20 @@ def test_load():
assert im.load()[0, 0] == (255, 255, 255) 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(): def test_invalid_file():
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
@ -404,3 +419,18 @@ def test_timeout(test_file):
with pytest.raises(Image.UnidentifiedImageError): with pytest.raises(Image.UnidentifiedImageError):
with Image.open(f): with Image.open(f):
pass 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

View File

@ -205,14 +205,14 @@ def test_optimize_full_l():
def test_optimize_if_palette_can_be_reduced_by_half(): def test_optimize_if_palette_can_be_reduced_by_half():
with Image.open("Tests/images/test.colors.gif") as im: im = Image.new("P", (8, 1))
# Reduce dimensions because original is too big for _get_optimize() im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
im = im.resize((591, 443)) for i in range(8):
im_rgb = im.convert("RGB") im.putpixel((i, 0), (i + 1, 0, 0))
for optimize, colors in ((False, 256), (True, 8)): for optimize, colors in ((False, 256), (True, 8)):
out = BytesIO() out = BytesIO()
im_rgb.save(out, "GIF", optimize=optimize) im.save(out, "GIF", optimize=optimize)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert len(reloaded.palette.palette) // 3 == colors assert len(reloaded.palette.palette) // 3 == colors
@ -590,7 +590,7 @@ def test_save_dispose(tmp_path):
def test_dispose2_palette(tmp_path): def test_dispose2_palette(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
# Four colors: white, grey, black, red # Four colors: white, gray, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
im_list = [] im_list = []
@ -1180,18 +1180,17 @@ def test_palette_save_L(tmp_path):
def test_palette_save_P(tmp_path): def test_palette_save_P(tmp_path):
# Pass in a different palette, then construct what the image would look like. im = Image.new("P", (1, 2))
# Forcing a non-straight grayscale palette. im.putpixel((0, 1), 1)
im = hopper("P")
palette = bytes(255 - i // 3 for i in range(768))
out = str(tmp_path / "temp.gif") 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: with Image.open(out) as reloaded:
im.putpalette(palette) reloaded_rgb = reloaded.convert("RGB")
assert_image_equal(reloaded, im)
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): def test_palette_save_duplicate_entries(tmp_path):

View File

@ -1,5 +1,7 @@
import sys import sys
from io import StringIO from io import BytesIO, StringIO
import pytest
from PIL import Image, IptcImagePlugin from PIL import Image, IptcImagePlugin
@ -30,6 +32,36 @@ def test_getiptcinfo_jpg_found():
assert iptc[(2, 101)] == b"Hungary" 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
pytest.fail("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(): def test_getiptcinfo_tiff_none():
# Arrange # Arrange
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:

View File

@ -767,6 +767,13 @@ class TestFileJpeg:
# This should return the default # This should return the default
assert im.info.get("dpi") == (72, 72) assert im.info.get("dpi") == (72, 72)
def test_dpi_exif_truncated(self):
# Arrange
with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
# Act / Assert
# This should return the default
assert im.info.get("dpi") == (72, 72)
def test_no_dpi_in_exif(self): def test_no_dpi_in_exif(self):
# Arrange # Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF: # This is photoshop-200dpi.jpg with resolution removed from EXIF:
@ -882,7 +889,10 @@ class TestFileJpeg:
def test_getxmp(self): def test_getxmp(self):
with Image.open("Tests/images/xmp_test.jpg") as im: with Image.open("Tests/images/xmp_test.jpg") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
xmp = im.getxmp() xmp = im.getxmp()
@ -905,6 +915,28 @@ class TestFileJpeg:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
assert im.getxmp() == {} assert im.getxmp() == {}
def test_getxmp_no_prefix(self):
with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {}
else:
assert im.getxmp() == {"xmpmeta": {"key": "value"}}
def test_getxmp_padded(self):
with Image.open("Tests/images/xmp_padded.jpg") as im:
if ElementTree is None:
with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {}
else:
assert im.getxmp() == {"xmpmeta": None}
@pytest.mark.timeout(timeout=1) @pytest.mark.timeout(timeout=1)
def test_eof(self): def test_eof(self):
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
@ -929,6 +961,28 @@ class TestFileJpeg:
im.load() im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_separate_tables(self):
im = hopper()
data = [] # [interchange, tables-only, image-only]
for streamtype in range(3):
out = BytesIO()
im.save(out, format="JPEG", streamtype=streamtype)
data.append(out.getvalue())
# SOI, EOI
for marker in b"\xff\xd8", b"\xff\xd9":
assert marker in data[1] and marker in data[2]
# DHT, DQT
for marker in b"\xff\xc4", b"\xff\xdb":
assert marker in data[1] and marker not in data[2]
# SOF0, SOS, APP0 (JFIF header)
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
assert marker not in data[1] and marker in data[2]
with Image.open(BytesIO(data[0])) as interchange_im:
with Image.open(BytesIO(data[1] + data[2])) as combined_im:
assert_image_equal(interchange_im, combined_im)
def test_repr_jpeg(self): def test_repr_jpeg(self):
im = hopper() im = hopper()

View File

@ -416,7 +416,7 @@ def test_plt_marker():
while True: while True:
marker = out.read(2) marker = out.read(2)
if not marker: if not marker:
assert False, "End of stream without PLT" pytest.fail("End of stream without PLT")
jp2_boxid = _binary.i16be(marker) jp2_boxid = _binary.i16be(marker)
if jp2_boxid == 0xFF4F: if jp2_boxid == 0xFF4F:
@ -426,7 +426,7 @@ def test_plt_marker():
# PLT # PLT
return return
elif jp2_boxid == 0xFF93: elif jp2_boxid == 0xFF93:
assert False, "SOD without finding PLT first" pytest.fail("SOD without finding PLT first")
hdr = out.read(2) hdr = out.read(2)
length = _binary.i16be(hdr) length = _binary.i16be(hdr)

View File

@ -26,8 +26,7 @@ def open_with_magick(magick, tmp_path, f):
rc = subprocess.call( rc = subprocess.call(
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
) )
if rc: assert not rc
raise OSError
return Image.open(outfile) return Image.open(outfile)

View File

@ -92,11 +92,11 @@ class TestFilePng:
assert im.format == "PNG" assert im.format == "PNG"
assert im.get_format_mimetype() == "image/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 = hopper(mode)
im.save(test_file) im.save(test_file)
with Image.open(test_file) as reloaded: with Image.open(test_file) as reloaded:
if mode == "I;16": if mode in ("I;16", "I;16B"):
reloaded = reloaded.convert(mode) reloaded = reloaded.convert(mode)
assert_image_equal(reloaded, im) assert_image_equal(reloaded, im)
@ -297,7 +297,7 @@ class TestFilePng:
assert_image(im, "RGBA", (10, 10)) assert_image(im, "RGBA", (10, 10))
assert im.getcolors() == [(100, (0, 0, 0, 0))] assert im.getcolors() == [(100, (0, 0, 0, 0))]
def test_save_greyscale_transparency(self, tmp_path): def test_save_grayscale_transparency(self, tmp_path):
for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
in_file = "Tests/images/" + mode.lower() + "_trns.png" in_file = "Tests/images/" + mode.lower() + "_trns.png"
with Image.open(in_file) as im: with Image.open(in_file) as im:
@ -665,7 +665,10 @@ class TestFilePng:
def test_getxmp(self): def test_getxmp(self):
with Image.open("Tests/images/color_snakes.png") as im: with Image.open("Tests/images/color_snakes.png") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
xmp = im.getxmp() xmp = im.getxmp()

View File

@ -734,7 +734,10 @@ class TestFileTiff:
def test_getxmp(self): def test_getxmp(self):
with Image.open("Tests/images/lab.tif") as im: with Image.open("Tests/images/lab.tif") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
xmp = im.getxmp() xmp = im.getxmp()

View File

@ -235,3 +235,13 @@ class TestFileWebp:
with Image.open(out_webp) as reloaded: with Image.open(out_webp) as reloaded:
reloaded.load() reloaded.load()
assert reloaded.info["duration"] == 1000 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)

View File

@ -118,7 +118,10 @@ def test_getxmp():
with Image.open("Tests/images/flower2.webp") as im: with Image.open("Tests/images/flower2.webp") as im:
if ElementTree is None: if ElementTree is None:
with pytest.warns(UserWarning): with pytest.warns(
UserWarning,
match="XMP data cannot be read without defusedxml dependency",
):
assert im.getxmp() == {} assert im.getxmp() == {}
else: else:
assert ( assert (

View File

@ -638,8 +638,8 @@ class TestImage:
im.remap_palette(None) im.remap_palette(None)
def test_remap_palette_transparency(self): def test_remap_palette_transparency(self):
im = Image.new("P", (1, 2)) im = Image.new("P", (1, 2), (0, 0, 0))
im.putpixel((0, 1), 1) im.putpixel((0, 1), (255, 0, 0))
im.info["transparency"] = 0 im.info["transparency"] = 0
im_remapped = im.remap_palette([1, 0]) im_remapped = im.remap_palette([1, 0])
@ -906,6 +906,38 @@ class TestImage:
im = Image.new("RGB", size) im = Image.new("RGB", size)
assert im.tobytes() == b"" assert im.tobytes() == b""
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_frombytes(self, size):
Image.frombytes("RGB", size, b"")
im = Image.new("RGB", size)
im.frombytes(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): def test_apply_transparency(self):
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
im.putpalette((0, 0, 0, 1, 1, 1)) im.putpalette((0, 0, 0, 1, 1, 1))
@ -967,7 +999,7 @@ class TestImage:
with Image.open(os.path.join("Tests/images", path)) as im: with Image.open(os.path.join("Tests/images", path)) as im:
try: try:
im.load() im.load()
assert False pytest.fail()
except OSError as e: except OSError as e:
buffer_overrun = str(e) == "buffer overrun when reading image file" buffer_overrun = str(e) == "buffer overrun when reading image file"
truncated = "image file is truncated" in str(e) truncated = "image file is truncated" in str(e)
@ -978,7 +1010,7 @@ class TestImage:
with Image.open("Tests/images/fli_overrun2.bin") as im: with Image.open("Tests/images/fli_overrun2.bin") as im:
try: try:
im.seek(1) im.seek(1)
assert False pytest.fail()
except OSError as e: except OSError as e:
assert str(e) == "buffer overrun when reading image file" assert str(e) == "buffer overrun when reading image file"

View File

@ -130,9 +130,16 @@ class TestImageGetPixel(AccessTest):
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
if bands == 1: if bands == 1:
return 1 return 1
if mode in ("BGR;15", "BGR;16"):
# These modes have less than 8 bits per band
# So (1, 2, 3) cannot be roundtripped
return (16, 32, 49)
return tuple(range(1, bands + 1)) return tuple(range(1, bands + 1))
def check(self, mode, expected_color=None): def check(self, mode, expected_color=None):
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
if not expected_color: if not expected_color:
expected_color = self.color(mode) expected_color = self.color(mode)
@ -203,6 +210,9 @@ class TestImageGetPixel(AccessTest):
"F", "F",
"P", "P",
"PA", "PA",
"BGR;15",
"BGR;16",
"BGR;24",
"RGB", "RGB",
"RGBA", "RGBA",
"RGBX", "RGBX",

View File

@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
im_l = im.convert("L") im_l = im.convert("L")
assert im_l.info["transparency"] == 1 # undone assert im_l.info["transparency"] == 0
im_l.save(f) im_l.save(f)
im_rgb = im.convert("RGB") 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) im_rgb.save(f)

View File

@ -76,6 +76,15 @@ def test_mode_F():
assert list(im.getdata()) == target assert list(im.getdata()) == target
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_mode_BGR(mode):
data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2))
im.putdata(data)
assert list(im.getdata()) == data
def test_array_B(): def test_array_B():
# shouldn't segfault # shouldn't segfault
# see https://github.com/python-pillow/Pillow/issues/1008 # see https://github.com/python-pillow/Pillow/issues/1008

View File

@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
im.putpalette(palette, mode) im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3] assert im.getpalette() == [1, 2, 3]
assert im.palette.colors == {(1, 2, 3, 4): 0} 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)

View File

@ -195,7 +195,7 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
) )
with pytest.raises(AssertionError): with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im) assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
@ -210,7 +210,7 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
) )
with pytest.raises(AssertionError): with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im) assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
@ -225,7 +225,7 @@ class TestReducingGapResize:
(52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
) )
with pytest.raises(AssertionError): with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im) assert_image_equal(ref, im)
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)

View File

@ -147,7 +147,7 @@ def test_reducing_gap_values():
ref = hopper() ref = hopper()
ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None) ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
with pytest.raises(AssertionError): with pytest.raises(pytest.fail.Exception):
assert_image_equal(ref, im) assert_image_equal(ref, im)
assert_image_similar(ref, im, 3.5) assert_image_similar(ref, im, 3.5)

View File

@ -10,7 +10,7 @@ GREEN = (0, 255, 0)
ORANGE = (255, 128, 0) ORANGE = (255, 128, 0)
WHITE = (255, 255, 255) WHITE = (255, 255, 255)
GREY = 128 GRAY = 128
def test_sanity(): def test_sanity():
@ -121,12 +121,12 @@ def test_constant():
im = Image.new("RGB", (20, 10)) im = Image.new("RGB", (20, 10))
# Act # Act
new = ImageChops.constant(im, GREY) new = ImageChops.constant(im, GRAY)
# Assert # Assert
assert new.size == im.size assert new.size == im.size
assert new.getpixel((0, 0)) == GREY assert new.getpixel((0, 0)) == GRAY
assert new.getpixel((19, 9)) == GREY assert new.getpixel((19, 9)) == GRAY
def test_darker_image(): def test_darker_image():

View File

@ -1,8 +1,9 @@
import contextlib
import os.path import os.path
import pytest import pytest
from PIL import Image, ImageColor, ImageDraw, ImageFont from PIL import Image, ImageColor, ImageDraw, ImageFont, features
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -586,6 +587,18 @@ def test_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") 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) @pytest.mark.parametrize("points", POINTS)
def test_polygon(points): def test_polygon(points):
# Arrange # Arrange
@ -732,7 +745,7 @@ def test_rectangle_I16(bbox):
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
# Act # Act
draw.rectangle(bbox, fill="black", outline="green") draw.rectangle(bbox, outline=0xFFFF)
# Assert # Assert
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
@ -1341,7 +1354,33 @@ def test_setting_default_font():
assert draw.getfont() == font assert draw.getfont() == font
finally: finally:
ImageDraw.ImageDraw.font = None ImageDraw.ImageDraw.font = None
assert isinstance(draw.getfont(), ImageFont.ImageFont) assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
def test_default_font_size():
freetype_support = features.check_module("freetype2")
text = "Default font at a specific size."
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
draw.text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
assert draw.textlength(text, font_size=16) == 216
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
im = Image.new("RGB", (220, 25))
draw = ImageDraw.Draw(im)
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
draw.multiline_text((0, 0), text, font_size=16)
assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)

View File

@ -141,7 +141,9 @@ def test_I16(font):
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
txt = "Hello World!" 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" target = "Tests/images/transparent_background_text_L.png"
assert_image_similar_tofile(im.convert("L"), target, 0.01) assert_image_similar_tofile(im.convert("L"), target, 0.01)
@ -301,8 +303,8 @@ def test_multiline_spacing(font):
"orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
) )
def test_rotated_transposed_font(font, orientation): def test_rotated_transposed_font(font, orientation):
img_grey = Image.new("L", (100, 100)) img_gray = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_grey) draw = ImageDraw.Draw(img_gray)
word = "testing" word = "testing"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation) transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@ -342,8 +344,8 @@ def test_rotated_transposed_font(font, orientation):
), ),
) )
def test_unrotated_transposed_font(font, orientation): def test_unrotated_transposed_font(font, orientation):
img_grey = Image.new("L", (100, 100)) img_gray = Image.new("L", (100, 100))
draw = ImageDraw.Draw(img_grey) draw = ImageDraw.Draw(img_gray)
word = "testing" word = "testing"
transposed_font = ImageFont.TransposedFont(font, orientation=orientation) transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
@ -451,7 +453,7 @@ def test_load_non_font_bytes():
def test_default_font(): def test_default_font():
# Arrange # Arrange
txt = 'This is a "better than nothing" default font.' txt = "This is a default font using FreeType support."
im = Image.new(mode="RGB", size=(300, 100)) im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -459,8 +461,11 @@ def test_default_font():
default_font = ImageFont.load_default() default_font = ImageFont.load_default()
draw.text((10, 10), txt, font=default_font) draw.text((10, 10), txt, font=default_font)
larger_default_font = ImageFont.load_default(size=14)
draw.text((10, 60), txt, font=larger_default_font)
# Assert # Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png") assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA")) @pytest.mark.parametrize("mode", (None, "1", "RGBA"))
@ -483,14 +488,6 @@ def test_render_empty(font):
assert_image_equal(im, target) assert_image_equal(im, target)
def test_unicode_pilfont():
# should not segfault, should return UnicodeDecodeError
# issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError):
font.getbbox("")
def test_unicode_extended(layout_engine): def test_unicode_extended(layout_engine):
# issue #3777 # issue #3777
text = "A\u278A\U0001F12B" text = "A\u278A\U0001F12B"
@ -720,14 +717,6 @@ def test_variation_set_by_axes(font):
_check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
def test_textbbox_non_freetypefont():
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"anchor, left, top", "anchor, left, top",
( (

View File

@ -0,0 +1,45 @@
import pytest
from PIL import Image, ImageDraw, ImageFont, features
from .helper import assert_image_equal_tofile
pytestmark = pytest.mark.skipif(
features.check_module("freetype2"),
reason="PILfont superseded if FreeType is supported",
)
def test_default_font():
# Arrange
txt = 'This is a "better than nothing" default font.'
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
# Act
default_font = ImageFont.load_default()
draw.text((10, 10), txt, font=default_font)
# Assert
assert_image_equal_tofile(im, "Tests/images/default_font.png")
def test_size_without_freetype():
with pytest.raises(ImportError):
ImageFont.load_default(size=14)
def test_unicode():
# should not segfault, should return UnicodeDecodeError
# issue #2826
font = ImageFont.load_default()
with pytest.raises(UnicodeEncodeError):
font.getbbox("")
def test_textbbox():
im = Image.new("RGB", (200, 200))
d = ImageDraw.Draw(im)
default_font = ImageFont.load_default()
assert d.textlength("test", font=default_font) == 24
assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)

View File

@ -39,6 +39,9 @@ def test_sanity():
ImageOps.contain(hopper("L"), (128, 128)) ImageOps.contain(hopper("L"), (128, 128))
ImageOps.contain(hopper("RGB"), (128, 128)) ImageOps.contain(hopper("RGB"), (128, 128))
ImageOps.cover(hopper("L"), (128, 128))
ImageOps.cover(hopper("RGB"), (128, 128))
ImageOps.crop(hopper("L"), 1) ImageOps.crop(hopper("L"), 1)
ImageOps.crop(hopper("RGB"), 1) ImageOps.crop(hopper("RGB"), 1)
@ -119,6 +122,20 @@ def test_contain_round():
assert new_im.height == 5 assert new_im.height == 5
@pytest.mark.parametrize(
"image_name, expected_size",
(
("colr_bungee.png", (1024, 256)), # landscape
("imagedraw_stroke_multiline.png", (256, 640)), # portrait
("hopper.png", (256, 256)), # square
),
)
def test_cover(image_name, expected_size):
with Image.open("Tests/images/" + image_name) as im:
new_im = ImageOps.cover(im, (256, 256))
assert new_im.size == expected_size
def test_pad(): def test_pad():
# Same ratio # Same ratio
im = hopper() im = hopper()
@ -416,6 +433,12 @@ def test_exif_transpose_in_place():
assert_image_equal(im, expected) assert_image_equal(im, expected)
def test_autocontrast_unsupported_mode():
im = Image.new("RGBA", (1, 1))
with pytest.raises(OSError):
ImageOps.autocontrast(im)
def test_autocontrast_cutoff(): def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast # Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img: with Image.open("Tests/images/bw_gradient.png") as img:

View File

@ -85,7 +85,7 @@ def test_ipythonviewer():
test_viewer = viewer test_viewer = viewer
break break
else: else:
assert False pytest.fail()
im = hopper() im = hopper()
assert test_viewer.show(im) == 1 assert test_viewer.show(im) == 1

View File

@ -340,6 +340,17 @@ class TestLibUnpack:
self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0)) self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5)) self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
self.assert_unpack(
"RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
)
def test_BGR(self):
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
self.assert_unpack(
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
)
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
def test_RGBA(self): def test_RGBA(self):
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
self.assert_unpack( self.assert_unpack(

View File

@ -1,6 +1,6 @@
import sys import sys
from setuptools.build_meta import * # noqa: F401, F403 from setuptools.build_meta import * # noqa: F403
from setuptools.build_meta import build_wheel from setuptools.build_meta import build_wheel
backend_class = build_wheel.__self__.__class__ backend_class = build_wheel.__self__.__class__

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install libimagequant # install libimagequant
archive=libimagequant-4.2.1 archive=libimagequant-4.2.2
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -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 .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _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 .. _GitHub: https://github.com/python-pillow/Pillow
.. _Python Package Index: https://pypi.org/project/Pillow/ .. _Python Package Index: https://pypi.org/project/Pillow/

View File

@ -318,14 +318,14 @@ def setup(app):
linkcheck_allowed_redirects = { linkcheck_allowed_redirects = {
r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501 r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501 r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501 r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501 r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501 r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501 r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501 r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
} }
# sphinx.ext.extlinks # sphinx.ext.extlinks
@ -338,6 +338,7 @@ extlinks = {
"cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
"issue": (_repo + "issues/%s", "#%s"), "issue": (_repo + "issues/%s", "#%s"),
"pr": (_repo + "pull/%s", "#%s"), "pr": (_repo + "pull/%s", "#%s"),
"pypi": ("https://pypi.org/project/%s/", "%s"),
} }
# sphinxext.opengraph # sphinxext.opengraph

View File

@ -10,7 +10,7 @@ Deprecated features
------------------- -------------------
Below are features which are considered deprecated. Where appropriate, Below are features which are considered deprecated. Where appropriate,
a ``DeprecationWarning`` is issued. a :py:exc:`DeprecationWarning` is issued.
PSFile PSFile
~~~~~~ ~~~~~~
@ -267,7 +267,7 @@ ImageFile.raise_ioerror
.. deprecated:: 7.2.0 .. deprecated:: 7.2.0
.. versionremoved:: 9.0.0 .. versionremoved:: 9.0.0
``IOError`` was merged into ``OSError`` in Python 3.3. :py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3.
So, ``ImageFile.raise_ioerror`` has been removed. So, ``ImageFile.raise_ioerror`` has been removed.
Use ``ImageFile.raise_oserror`` instead. Use ``ImageFile.raise_oserror`` instead.
@ -293,9 +293,9 @@ im.offset
``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead. ``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
It was documented as deprecated in PIL 1.1.2, It was documented as deprecated in PIL 1.1.2,
raised a ``DeprecationWarning`` since 1.1.5, raised a :py:exc:`DeprecationWarning` since 1.1.5,
an ``Exception`` since Pillow 3.0.0 an :py:exc:`Exception` since Pillow 3.0.0
and ``NotImplementedError`` since 3.3.0. and :py:exc:`NotImplementedError` since 3.3.0.
Image.fromstring, im.fromstring and im.tostring Image.fromstring, im.fromstring and im.tostring
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -307,9 +307,9 @@ Image.fromstring, im.fromstring and im.tostring
* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead. * ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead. * ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
They issued a ``DeprecationWarning`` since 2.0.0, They issued a :py:exc:`DeprecationWarning` since 2.0.0,
an ``Exception`` since 3.0.0 an :py:exc:`Exception` since 3.0.0
and ``NotImplementedError`` since 3.3.0. and :py:exc:`NotImplementedError` since 3.3.0.
ImageCms.CmsProfile attributes ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -318,7 +318,7 @@ ImageCms.CmsProfile attributes
.. versionremoved:: 8.0.0 .. versionremoved:: 8.0.0
Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
they issued a ``DeprecationWarning``: they issued a :py:exc:`DeprecationWarning`:
======================== =================================================== ======================== ===================================================
Removed Use instead Removed Use instead
@ -442,7 +442,7 @@ PIL.OleFileIO
.. deprecated:: 4.0.0 .. deprecated:: 4.0.0
.. versionremoved:: 6.0.0 .. versionremoved:: 6.0.0
PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of ``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0 the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from (2018-01). The deprecated file has now been removed from Pillow. If needed, install from
PyPI (eg. ``python3 -m pip install olefile``). PyPI (eg. ``python3 -m pip install olefile``).

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -63,8 +63,35 @@ DDS
^^^ ^^^
DDS is a popular container texture format used in video games and natively supported 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, by DirectX.
DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode.
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 DIB
^^^ ^^^
@ -88,8 +115,13 @@ in ``L``, ``RGB`` and ``CMYK`` modes.
Loading 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` 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** **scale**
Affects the scale of the resultant rasterized image. If the EPS suggests Affects the scale of the resultant rasterized image. If the EPS suggests
@ -1282,6 +1314,8 @@ Pillow reads Kodak FlashPix files. In the current version, only the highest
resolution image is read from the file, and the viewing transform is not taken resolution image is read from the file, and the viewing transform is not taken
into account. into account.
To enable FPX support, you must install :pypi:`olefile`.
.. note:: .. note::
To enable full FlashPix support, you need to build and install the IJG JPEG To enable full FlashPix support, you need to build and install the IJG JPEG
@ -1358,6 +1392,8 @@ the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.s
Note that there may be an embedded gamma of 2.2 in MIC files. Note that there may be an embedded gamma of 2.2 in MIC files.
To enable MIC support, you must install :pypi:`olefile`.
MPO MPO
^^^ ^^^

View File

@ -26,7 +26,7 @@ image. If the image was not read from a file, it is set to None. The size
attribute is a 2-tuple containing width and height (in pixels). The attribute is a 2-tuple containing width and height (in pixels). The
:py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the :py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the
bands in the image, and also the pixel type and depth. Common modes are “L” bands in the image, and also the pixel type and depth. Common modes are “L”
(luminance) for greyscale images, “RGB” for true color images, and “CMYK” for (luminance) for grayscale images, “RGB” for true color images, and “CMYK” for
pre-press images. pre-press images.
If the file cannot be opened, an :py:exc:`OSError` exception is raised. If the file cannot be opened, an :py:exc:`OSError` exception is raised.
@ -268,6 +268,37 @@ true, to provide for the same changes to the image's size.
A more general form of image transformations can be carried out via the A more general form of image transformations can be carried out via the
:py:meth:`~PIL.Image.Image.transform` method. :py:meth:`~PIL.Image.Image.transform` method.
Relative resizing
^^^^^^^^^^^^^^^^^
Instead of calculating the size of the new image when resizing, you can also
choose to resize relative to a given size.
::
from PIL import Image, ImageOps
size = (100, 150)
with Image.open("Tests/images/hopper.png") as im:
ImageOps.contain(im, size).save("imageops_contain.png")
ImageOps.cover(im, size).save("imageops_cover.png")
ImageOps.fit(im, size).save("imageops_fit.png")
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
# thumbnail() can also be used,
# but will modify the image object in place
im.thumbnail(size)
im.save("imageops_thumbnail.png")
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
+================+===========================================+============================================+==========================================+========================================+========================================+
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
.. _color-transforms: .. _color-transforms:
Color transforms Color transforms
@ -568,7 +599,7 @@ Controlling the decoder
Some decoders allow you to manipulate the image while reading it from a file. Some decoders allow you to manipulate the image while reading it from a file.
This can often be used to speed up decoding when creating thumbnails (when This can often be used to speed up decoding when creating thumbnails (when
speed is usually more important than quality) and printing to a monochrome speed is usually more important than quality) and printing to a monochrome
laser printer (when only a greyscale version of the image is needed). laser printer (when only a grayscale version of the image is needed).
The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet
loaded image so it as closely as possible matches the given mode and size. This loaded image so it as closely as possible matches the given mode and size. This

View File

@ -26,14 +26,14 @@ Pillow decodes files in two stages:
it. it.
An image plugin should contain a format handler derived from the An image plugin should contain a format handler derived from the
:py:class:`PIL.ImageFile.ImageFile` base class. This class should :py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an
provide an ``_open`` method, which reads the file header and ``_open`` method, which reads the file header and set at least the internal
sets up at least the :py:attr:`~PIL.Image.Image.mode` and ``_size`` and ``_mode`` attributes so that :py:attr:`~PIL.Image.Image.mode` and
:py:attr:`~PIL.Image.Image.size` attributes. To be able to load the :py:attr:`~PIL.Image.Image.size` are populated. To be able to load the file,
file, the method must also create a list of ``tile`` descriptors, the method must also create a list of ``tile`` descriptors, which contain a
which contain a decoder name, extents of the tile, and decoder name, extents of the tile, and any decoder-specific data. The format
any decoder-specific data. The format handler class must be explicitly handler class must be explicitly registered, via a call to the
registered, via a call to the :py:mod:`~PIL.Image` module. :py:mod:`~PIL.Image` module.
.. note:: For performance reasons, it is important that the .. note:: For performance reasons, it is important that the
``_open`` method quickly rejects files that do not have the ``_open`` method quickly rejects files that do not have the
@ -45,7 +45,7 @@ Example
The following plugin supports a simple format, which has a 128-byte header The following plugin supports a simple format, which has a 128-byte header
consisting of the words “SPAM” followed by the width, height, and pixel size in consisting of the words “SPAM” followed by the width, height, and pixel size in
bits. The header fields are separated by spaces. The image data follows bits. The header fields are separated by spaces. The image data follows
directly after the header, and can be either bi-level, greyscale, or 24-bit directly after the header, and can be either bi-level, grayscale, or 24-bit
true color. true color.
**SpamImagePlugin.py**:: **SpamImagePlugin.py**::
@ -96,13 +96,13 @@ true color.
) )
The format handler must always set the The format handler must always set the internal ``_size`` and ``_mode``
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.mode` attributes so that :py:attr:`~PIL.Image.Image.size` and
attributes. If these are not set, the file cannot be opened. To :py:attr:`~PIL.Image.Image.mode` are populated. If these are not set, the file
simplify the plugin, the calling code considers exceptions like cannot be opened. To simplify the plugin, the calling code considers exceptions
:py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`, like :py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`,
:py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify :py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify the
the file. file.
Note that the image plugin must be explicitly registered using Note that the image plugin must be explicitly registered using
:py:func:`PIL.Image.register_open`. Although not required, it is also a good :py:func:`PIL.Image.register_open`. Although not required, it is also a good
@ -211,9 +211,9 @@ table describes some commonly used **raw modes**:
| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the | | ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the |
| | | least significant bit. 0 means black, 1 means white. | | | | least significant bit. 0 means black, 1 means white. |
+-----------+-------------------------------------------------------------------+ +-----------+-------------------------------------------------------------------+
| ``L`` | 8-bit greyscale. 0 means black, 255 means white. | | ``L`` | 8-bit grayscale. 0 means black, 255 means white. |
+-----------+-------------------------------------------------------------------+ +-----------+-------------------------------------------------------------------+
| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. | | ``L;I`` | 8-bit inverted grayscale. 0 means white, 255 means black. |
+-----------+-------------------------------------------------------------------+ +-----------+-------------------------------------------------------------------+
| ``P`` | 8-bit palette-mapped image. | | ``P`` | 8-bit palette-mapped image. |
+-----------+-------------------------------------------------------------------+ +-----------+-------------------------------------------------------------------+

View File

@ -37,12 +37,12 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://ci.appveyor.com/project/python-pillow/Pillow :target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows) :alt: AppVeyor CI build status (Windows)
.. image:: https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg .. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
:target: https://github.com/python-pillow/pillow-wheels/actions :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions wheels build status (Wheels) :alt: GitHub Actions build status (Wheels)
.. image:: https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels .. image:: https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels
:target: https://app.travis-ci.com/github/python-pillow/pillow-wheels :target: https://app.travis-ci.com/github/python-pillow/Pillow
:alt: Travis CI wheels build status (aarch64) :alt: Travis CI wheels build status (aarch64)
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg .. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
@ -69,8 +69,8 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/Pillow/
:alt: Number of PyPI downloads :alt: Number of PyPI downloads
.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge .. image:: https://www.bestpractices.dev/projects/6331/badge
:target: https://bestpractices.coreinfrastructure.org/projects/6331 :target: https://www.bestpractices.dev/projects/6331
:alt: OpenSSF Best Practices :alt: OpenSSF Best Practices
.. image:: https://badges.gitter.im/python-pillow/Pillow.svg .. image:: https://badges.gitter.im/python-pillow/Pillow.svg

View File

@ -42,6 +42,11 @@ Install Pillow with :command:`pip`::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow python3 -m pip install --upgrade Pillow
Optionally, install :pypi:`defusedxml` for Pillow to read XMP data,
and :pypi:`olefile` for Pillow to read FPX and MIC images::
python3 -m pip install --upgrade defusedxml olefile
.. tab:: Linux .. tab:: Linux
@ -82,6 +87,8 @@ Install Pillow with :command:`pip`::
.. tab:: Windows .. tab:: Windows
.. warning:: Pillow > 9.5.0 no longer includes 32-bit wheels.
We provide Pillow binaries for Windows compiled for the matrix of We provide Pillow binaries for Windows compiled for the matrix of
supported Pythons in 64-bit versions in the wheel format. These binaries include supported Pythons in 64-bit versions in the wheel format. These binaries include
support for all optional libraries except libimagequant and libxcb. Raqm support support for all optional libraries except libimagequant and libxcb. Raqm support
@ -154,7 +161,7 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality * **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1** * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
* **libfreetype** provides type related services * **libfreetype** provides type related services
@ -180,7 +187,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.2.1** * Pillow has been tested with libimagequant **2.6-4.2.2**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.
@ -354,7 +361,7 @@ for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
additional configuration should be required. If they are installed in additional configuration should be required. If they are installed in
a non-standard location, you may need to configure setuptools to use a non-standard location, you may need to configure setuptools to use
those locations by editing :file:`setup.py` or those locations by editing :file:`setup.py` or
:file:`setup.cfg`, or by adding environment variables on the command :file:`pyproject.toml`, or by adding environment variables on the command
line:: line::
CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
@ -454,10 +461,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 | | Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 | | Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 | | Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | | macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 |
@ -476,7 +483,7 @@ These platforms are built and tested for every change.
| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | | Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, PyPy3 | | | | 3.12, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.11 | x86 | | | 3.12 | x86 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9 (MinGW) | x86-64 | | | 3.9 (MinGW) | x86-64 |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
@ -494,91 +501,93 @@ These platforms have been reported to work at the versions mentioned.
Contributors please test Pillow on your platform then update this Contributors please test Pillow on your platform then update this
document and send a pull request. document and send a pull request.
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Operating system | | Tested Python | | Latest tested | | Tested | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+ +==================================+============================+==================+==============+
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm | | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0 |arm |
| +---------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.7 | 9.5.0 | | | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
+----------------------------------+---------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | | | 3.7 | 9.5.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
| +---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 | | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+ | | +----------------------------+------------------+--------------+
| | 3.6 | 8.4.0 | | | | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | | | 3.6 | 8.4.0 | |
| +---------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.5 | 7.2.0 | | | macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | | | 3.5 | 7.2.0 | |
| +---------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 2.7 | 6.0.0 | | | macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
| +---------------------------+------------------+ | | +----------------------------+------------------+ |
| | 3.4 | 5.4.1 | | | | 2.7 | 6.0.0 | |
+----------------------------------+---------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | | | 3.4 | 5.4.1 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 | | macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | | macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
| +---------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 3.3 | 4.1.0 | | | Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 | | | 3.3 | 4.1.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 | | Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Redhat Linux 6 | 2.6 | |x86 | | Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| CentOS 6.3 | 2.7, 3.3 | |x86 | | Redhat Linux 6 | 2.6 | |x86 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| CentOS 8 | 3.9 | 9.0.0 |x86-64 | | CentOS 6.3 | 2.7, 3.3 | |x86 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | | CentOS 8 | 3.9 | 9.0.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | | Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
| | | PyPy5.3.1, PyPy3 v2.4.0 | | | +----------------------------------+----------------------------+------------------+--------------+
| +---------------------------+------------------+--------------+ | Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
| | 2.7 | 4.3.0 |x86-64 | | | | PyPy5.3.1, PyPy3 v2.4.0 | | |
| +---------------------------+------------------+--------------+ | +----------------------------+------------------+--------------+
| | 2.7, 3.2 | 3.4.1 |ppc | | | 2.7 | 4.3.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ | +----------------------------+------------------+--------------+
| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | | | 2.7, 3.2 | 3.4.1 |ppc |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | | Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | | Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | | Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | | Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
| +---------------------------+------------------+ | +----------------------------------+----------------------------+------------------+--------------+
| | 2.7 | 6.2.2 | | | Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ | +----------------------------+------------------+ |
| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | | | 2.7 | 6.2.2 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 | | Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 | | FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 | | FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 10 | 3.7 | 7.1.0 |x86-64 | | FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 | | Windows 10 | 3.7 | 7.1.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 | | Windows 10/Cygwin 3.3 | 3.6, 3.7, 3.8, 3.9 | 8.4.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 | | Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 | | Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | | Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+----------------------------+------------------+--------------+
| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
+----------------------------------+----------------------------+------------------+--------------+
Old Versions Old Versions
------------ ------------

View File

@ -351,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` .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell`
.. autoattribute:: PIL.Image.Image.has_transparency_data
Classes Classes
------- -------

View File

@ -59,7 +59,7 @@ Functions
.. py:method:: getcolor(color, mode) .. py:method:: getcolor(color, mode)
Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
greyscale value if the mode is not color or a palette image. If the string grayscale value if the mode is not color or a palette image. If the string
cannot be parsed, this function raises a :py:exc:`ValueError` exception. cannot be parsed, this function raises a :py:exc:`ValueError` exception.
.. versionadded:: 1.1.4 .. versionadded:: 1.1.4

View File

@ -351,7 +351,7 @@ Methods
Draw a shape. Draw a shape.
.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) .. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
Draws the string at the given position. Draws the string at the given position.
@ -416,8 +416,14 @@ Methods
.. versionadded:: 8.0.0 .. versionadded:: 8.0.0
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.
.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False) .. versionadded:: 10.1.0
.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
Draws the string at the given position. Draws the string at the given position.
@ -477,7 +483,13 @@ Methods
.. versionadded:: 8.0.0 .. versionadded:: 8.0.0
.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) :param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.
.. versionadded:: 10.1.0
.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False, font_size=None)
Returns length (in pixels with 1/64 precision) of given text when rendered Returns length (in pixels with 1/64 precision) of given text when rendered
in font with provided direction, features, and language. in font with provided direction, features, and language.
@ -538,9 +550,15 @@ Methods
It should be a `BCP 47 language code`_. It should be a `BCP 47 language code`_.
Requires libraqm. Requires libraqm.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.
.. versionadded:: 10.1.0
:return: Either width for horizontal text, or 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) .. 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, font_size=None)
Returns bounding box (in pixels) of given text relative to given anchor Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language. when rendered in font with provided direction, features, and language.
@ -588,9 +606,15 @@ Methods
Requires libraqm. Requires libraqm.
:param stroke_width: The width of the text stroke. :param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.
.. versionadded:: 10.1.0
:return: ``(left, top, right, bottom)`` bounding box :return: ``(left, top, right, bottom)`` bounding box
.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) .. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None)
Returns bounding box (in pixels) of given text relative to given anchor Returns bounding box (in pixels) of given text relative to given anchor
when rendered in font with provided direction, features, and language. when rendered in font with provided direction, features, and language.
@ -632,6 +656,12 @@ Methods
Requires libraqm. Requires libraqm.
:param stroke_width: The width of the text stroke. :param stroke_width: The width of the text stroke.
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
:param font_size: If ``font`` is not provided, then the size to use for the default
font.
Keyword-only argument.
.. versionadded:: 10.1.0
:return: ``(left, top, right, bottom)`` bounding box :return: ``(left, top, right, bottom)`` bounding box
.. py:method:: getdraw(im=None, hints=None) .. py:method:: getdraw(im=None, hints=None)

View File

@ -58,7 +58,7 @@ method:
This class can be used to control the contrast of an image, similar to the This class can be used to control the contrast of an image, similar to the
contrast control on a TV set. An contrast control on a TV set. An
:ref:`enhancement factor <enhancement-factor>` of 0.0 gives a solid grey :ref:`enhancement factor <enhancement-factor>` of 0.0 gives a solid gray
image, a factor of 1.0 gives the original image, and greater values image, a factor of 1.0 gives the original image, and greater values
increase the contrast of the image. increase the contrast of the image.

View File

@ -10,7 +10,7 @@ this class store bitmap fonts, and are used with the
PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use
`pilfont.py <https://github.com/python-pillow/pillow-scripts/blob/main/Scripts/pilfont.py>`_ `pilfont.py <https://github.com/python-pillow/pillow-scripts/blob/main/Scripts/pilfont.py>`_
from `pillow-scripts <https://pypi.org/project/pillow-scripts/>`_ to convert BDF and from :pypi:`pillow-scripts` to convert BDF and
PCF font descriptors (X window font formats) to this format. PCF font descriptors (X window font formats) to this format.
Starting with version 1.1.4, PIL can be configured to support TrueType and Starting with version 1.1.4, PIL can be configured to support TrueType and
@ -20,7 +20,7 @@ the imToolkit package.
.. warning:: .. warning::
To protect against potential DOS attacks when using arbitrary strings as To protect against potential DOS attacks when using arbitrary strings as
text input, Pillow will raise a ``ValueError`` if the number of characters text input, Pillow will raise a :py:exc:`ValueError` if the number of characters
is over a certain limit, :py:data:`MAX_STRING_LENGTH`. is over a certain limit, :py:data:`MAX_STRING_LENGTH`.
This threshold can be changed by setting This threshold can be changed by setting
@ -89,5 +89,5 @@ Constants
.. data:: MAX_STRING_LENGTH .. data:: MAX_STRING_LENGTH
Set to 1,000,000, to protect against potential DOS attacks. Pillow will Set to 1,000,000, to protect against potential DOS attacks. Pillow will
raise a ``ValueError`` if the number of characters is over this limit. The raise a :py:exc:`ValueError` if the number of characters is over this limit. The
check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``.

View File

@ -72,7 +72,7 @@ pixel bits.
Note that the operands are converted to 32-bit signed integers before the Note that the operands are converted to 32-bit signed integers before the
bitwise operation is applied. This means that youll get negative values if bitwise operation is applied. This means that youll get negative values if
you invert an ordinary greyscale image. You can use the and (&) operator to you invert an ordinary grayscale image. You can use the and (&) operator to
mask off unwanted bits. mask off unwanted bits.
Bitwise operators dont work on floating point images. Bitwise operators dont work on floating point images.

View File

@ -12,14 +12,11 @@ only work on L and RGB images.
.. autofunction:: autocontrast .. autofunction:: autocontrast
.. autofunction:: colorize .. autofunction:: colorize
.. autofunction:: contain
.. autofunction:: pad
.. autofunction:: crop .. autofunction:: crop
.. autofunction:: scale .. autofunction:: scale
.. autofunction:: deform .. autofunction:: deform
.. autofunction:: equalize .. autofunction:: equalize
.. autofunction:: expand .. autofunction:: expand
.. autofunction:: fit
.. autofunction:: flip .. autofunction:: flip
.. autofunction:: grayscale .. autofunction:: grayscale
.. autofunction:: invert .. autofunction:: invert
@ -27,3 +24,38 @@ only work on L and RGB images.
.. autofunction:: posterize .. autofunction:: posterize
.. autofunction:: solarize .. autofunction:: solarize
.. autofunction:: exif_transpose .. autofunction:: exif_transpose
.. _relative-resize:
Resize relative to a given size
-------------------------------
::
from PIL import Image, ImageOps
size = (100, 150)
with Image.open("Tests/images/hopper.png") as im:
ImageOps.contain(im, size).save("imageops_contain.png")
ImageOps.cover(im, size).save("imageops_cover.png")
ImageOps.fit(im, size).save("imageops_fit.png")
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
# thumbnail() can also be used,
# but will modify the image object in place
im.thumbnail(size)
im.save("imageops_thumbnail.png")
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
+================+===========================================+============================================+==========================================+========================================+========================================+
|Given size | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` | ``(100, 150)`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``100×150`` | ``100×150`` |
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
.. autofunction:: contain
.. autofunction:: cover
.. autofunction:: fit
.. autofunction:: pad

View File

@ -173,8 +173,8 @@ been processed before Pillow started checking for decompression bombs.
Added ImageFont.MAX_STRING_LENGTH Added ImageFont.MAX_STRING_LENGTH
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To protect against potential DOS attacks when using arbitrary strings as text :cve:`2023-44271`: To protect against potential DOS attacks when using arbitrary strings as text
input, Pillow will now raise a ``ValueError`` if the number of characters input, Pillow will now raise a :py:exc:`ValueError` if the number of characters
passed into ImageFont methods is over a certain limit, passed into ImageFont methods is over a certain limit,
:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`.

View File

@ -1,54 +1,109 @@
10.1.0 10.1.0
------ ------
Backwards Incompatible Changes API Changes
============================== ===========
Setting image mode Setting image mode
^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^
If you attempt to set the mode of an image directly, e.g. If you attempt to set the mode of an image directly, e.g.
``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is ``im.mode = "RGBA"``, you will now receive an :py:exc:`AttributeError`. This is
not about removing existing functionality, but instead about raising an not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``convert`` method is the explicit error to prevent later consequences. The ``convert`` method is the
correct way to change an image's mode. correct way to change an image's mode.
Deprecations Accept a list in getpixel()
============ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO :py:meth:`~PIL.Image.Image.getpixel` now accepts a list of coordinates, as well
^^^^ as a tuple. ::
TODO from PIL import Image
im = Image.new("RGB", (1, 1))
im.getpixel((0, 0))
im.getpixel([0, 0])
API Changes BoxBlur and GaussianBlur allow for different x and y radii
=========== ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TODO :py:class:`~PIL.ImageFilter.BoxBlur` and
^^^^ :py:class:`~PIL.ImageFilter.GaussianBlur` now allow a sequence of x and y radii
to be specified, rather than a single number for both dimensions. ::
TODO from PIL import ImageFilter
ImageFilter.BoxBlur((2, 5))
ImageFilter.GaussianBlur((2, 5))
API Additions API Additions
============= =============
TODO EpsImagePlugin.gs_binary
^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
TODO ``EpsImagePlugin.gs_windows_binary`` stores the name of the Ghostscript
executable on Windows. ``EpsImagePlugin.gs_binary`` has now been added for all
platforms, and can be used to customise the name of the executable, or disable
use entirely through ``EpsImagePlugin.gs_binary = False``.
Security has_transparency_data
======== ^^^^^^^^^^^^^^^^^^^^^
TODO 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.
TODO Even if this attribute is true, the image might still appear solid, if all of
the values shown within are opaque.
ImageOps.cover
^^^^^^^^^^^^^^
Returns a resized version of the image, so that the requested size is covered,
while maintaining the original aspect ratio.
See :ref:`relative-resize` for a comparison between this and similar ``ImageOps``
methods.
size and font_size arguments when using default font
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow has had a "better than nothing" default font, which can only be drawn at
one font size. Now, if FreeType support is available, a version of
`Aileron Regular <https://dotcolon.net/font/aileron>`_ is loaded, which can be
drawn at chosen font sizes.
The following ``size`` and ``font_size`` arguments can now be used to specify a
font size for this new builtin font::
ImageFont.load_default(size=24)
draw.text((0, 0), "test", font_size=24)
draw.textlength((0, 0), "test", font_size=24)
draw.textbbox((0, 0), "test", font_size=24)
draw.multiline_text((0, 0), "test", font_size=24)
draw.multiline_textbbox((0, 0), "test", font_size=24)
Other Changes Other Changes
============= =============
TODO Python 3.12
^^^^ ^^^^^^^^^^^
TODO Pillow 10.0.0 had wheels built against Python 3.12 beta, available as a preview to help
others prepare for 3.12, and to ensure Pillow could be used immediately at the release
of 3.12.0 final (2023-10-02, :pep:`693`).
Pillow 10.1.0 now officially supports Python 3.12.
Added support for DDS BC5U and 8-bit color indexed images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support has been added to read BC5U DDS files as RGB images, and
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.

View File

@ -10,7 +10,7 @@ operations. As a result PIL was unable to open them as images, requiring a wrap
``cStringIO`` or ``BytesIO``. ``cStringIO`` or ``BytesIO``.
Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and
catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we catch on exception :py:exc:`AttributeError` or :py:exc:`io.UnsupportedOperation`. If this is caught we
attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like
objects). objects).

View File

@ -19,7 +19,7 @@ Deprecation Warning when Saving JPEGs
JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0 JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0
silently drops the alpha channel. With this release Pillow will now silently drops the alpha channel. With this release Pillow will now
issue a ``DeprecationWarning`` when attempting to save a ``RGBA`` mode issue a :py:exc:`DeprecationWarning` when attempting to save a ``RGBA`` mode
image as a JPEG. This will become an error in Pillow 4.2. image as a JPEG. This will become an error in Pillow 4.2.
New DDS Decoders New DDS Decoders

View File

@ -17,8 +17,8 @@ Pillow 4.0 supports Python 3.6.
OleFileIO.py OleFileIO.py
============ ============
OleFileIO.py has been removed as a vendored file and is now installed ``OleFileIO.py`` has been removed as a vendored file and is now installed
from the upstream olefile pypi package. All internal dependencies are from the upstream :pypi:`olefile` PyPI package. All internal dependencies are
redirected to the olefile package. Direct accesses to redirected to the olefile package. Direct accesses to
``PIL.OlefileIO`` raises a deprecation warning, then patches the ``PIL.OlefileIO`` raises a deprecation warning, then patches the
upstream olefile into ``sys.modules`` in its place. upstream olefile into ``sys.modules`` in its place.

View File

@ -28,7 +28,7 @@ Scripts
The scripts formerly installed by Pillow have been split into a The scripts formerly installed by Pillow have been split into a
separate package, pillow-scripts, living at separate package, pillow-scripts, living at
https://github.com/python-pillow/pillow-scripts . https://github.com/python-pillow/pillow-scripts.
API Changes API Changes
@ -37,7 +37,7 @@ API Changes
OleFileIO.py OleFileIO.py
^^^^^^^^^^^^ ^^^^^^^^^^^^
The olefile module is no longer a required dependency when installing Pillow. The :pypi:`olefile` module is no longer a required dependency when installing Pillow.
Support for plugins requiring olefile will not be loaded if it is not Support for plugins requiring olefile will not be loaded if it is not
installed. This allows library consumers to avoid installing this dependency installed. This allows library consumers to avoid installing this dependency
if they choose. Some library consumers have little interest in the format if they choose. Some library consumers have little interest in the format

View File

@ -8,7 +8,7 @@ Image size
^^^^^^^^^^ ^^^^^^^^^^
If you attempt to set the size of an image directly, e.g. If you attempt to set the size of an image directly, e.g.
``im.size = (100, 100)``, you will now receive an ``AttributeError``. This is ``im.size = (100, 100)``, you will now receive an :py:exc:`AttributeError`. This is
not about removing existing functionality, but instead about raising an not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``resize`` method is the explicit error to prevent later consequences. The ``resize`` method is the
correct way to change an image's size. correct way to change an image's size.
@ -16,7 +16,8 @@ correct way to change an image's size.
The exceptions to this are: The exceptions to this are:
* The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage. * The ICO and ICNS image formats, which use ``im.size = (100, 100)`` to select a subimage.
* The TIFF image format, which now has a ``DeprecationWarning`` for this action, as direct image size setting was previously necessary to work around an issue with tile extents. * The TIFF image format, which now has a :py:exc:`DeprecationWarning` for this action,
as direct image size setting was previously necessary to work around an issue with tile extents.
API Additions API Additions

View File

@ -15,7 +15,7 @@ PNG: Handle IDAT chunks after image end
Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop Some PNG images have multiple IDAT chunks. In some cases, Pillow will stop
reading image data before the IDAT chunks finish. A regression caused an reading image data before the IDAT chunks finish. A regression caused an
``EOFError`` exception when previously there was none. This is now fixed, and :py:exc:`EOFError` exception when previously there was none. This is now fixed, and
file reading continues in case there are subsequent text chunks. file reading continues in case there are subsequent text chunks.
PNG: MIME type PNG: MIME type
@ -30,7 +30,7 @@ File closing
^^^^^^^^^^^^ ^^^^^^^^^^^^
A regression caused an unsupported image file to report a A regression caused an unsupported image file to report a
``ValueError: seek of closed file`` exception instead of an ``OSError``. This ``ValueError: seek of closed file`` exception instead of an :py:exc:`OSError`. This
has been fixed by ensuring that image plugins only close their internal ``__fp`` has been fixed by ensuring that image plugins only close their internal ``__fp``
if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own if they are not the same as ``ImageFile``'s ``fp``, allowing each to manage their own
file pointers. file pointers.

View File

@ -14,8 +14,8 @@ Pillow for Python 3.4 is 5.4.1.
Removed deprecated PIL.OleFileIO Removed deprecated PIL.OleFileIO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of ``PIL.OleFileIO`` was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
the upstream olefile Python package, and replaced with an ``ImportError``. The the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError`. The
deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. deprecated file has now been removed from Pillow. If needed, install from PyPI (eg.
``python3 -m pip install olefile``). ``python3 -m pip install olefile``).
@ -103,7 +103,7 @@ ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From
6.0.0, they issue a ``DeprecationWarning``: 6.0.0, they issue a :py:exc:`DeprecationWarning`:
======================== =============================== ======================== ===============================
Deprecated Use instead Deprecated Use instead

View File

@ -29,10 +29,10 @@ API Additions
Image.entropy Image.entropy
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated Calculates and returns the entropy for the image. A bilevel image (mode "1") is treated
as a greyscale ("L") image by this method. If a mask is provided, the method employs as a grayscale ("L") image by this method. If a mask is provided, the method employs
the histogram for those parts of the image where the mask image is non-zero. The mask the histogram for those parts of the image where the mask image is non-zero. The mask
image must have the same size as the image, and be either a bi-level image (mode "1") or image must have the same size as the image, and be either a bi-level image (mode "1") or
a greyscale image ("L"). a grayscale image ("L").
ImageGrab.grab ImageGrab.grab
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
@ -58,7 +58,7 @@ file. ``ImageFont.FreeTypeFont`` has four new methods,
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and :py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_name` for using named styles, and
:py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and :py:meth:`PIL.ImageFont.FreeTypeFont.get_variation_axes` and
:py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes :py:meth:`PIL.ImageFont.FreeTypeFont.set_variation_by_axes` for using font axes
instead. An ``IOError`` will be raised if the font is not a variation font. FreeType instead. An :py:exc:`IOError` will be raised if the font is not a variation font. FreeType
2.9.1 or greater is required. 2.9.1 or greater is required.
Other Changes Other Changes

View File

@ -85,7 +85,7 @@ Custom unidentified image error
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be Pillow will now throw a custom ``UnidentifiedImageError`` when an image cannot be
identified. For backwards compatibility, this will inherit from ``OSError``. identified. For backwards compatibility, this will inherit from :py:exc:`OSError`.
New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods New argument ``reducing_gap`` for Image.resize() and Image.thumbnail() methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Some files were not shown because too many files have changed in this diff Show More