Merge branch 'main' into jpeg_xmp

This commit is contained in:
Andrew Murray 2023-10-06 17:23:53 +11:00
commit f24222a954
169 changed files with 2478 additions and 747 deletions

View File

@ -21,13 +21,11 @@ environment:
install: install:
- '%PYTHON%\%EXECUTABLE% --version' - '%PYTHON%\%EXECUTABLE% --version'
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' - '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
- 7z x pillow-depends.zip -oc:\
- 7z x pillow-test-images.zip -oc:\ - 7z x pillow-test-images.zip -oc:\
- mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\ - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
- 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.0.0.20230317 - choco install ghostscript --version=10.0.0.20230317
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\

View File

@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard sway wl-clipboard libopenblas-dev
fi fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
@ -38,11 +38,10 @@ 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
# TODO Remove condition when NumPy supports 3.12 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6 python3 -m pip install pyqt6
fi fi

View File

@ -28,7 +28,7 @@ jobs:
name: Docs name: Docs
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4

View File

@ -17,7 +17,7 @@ jobs:
name: Lint name: Lint
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v3 uses: actions/cache@v3

View File

@ -3,6 +3,7 @@
set -e 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"
PYTHONOPTIMIZE=0 python3 -m pip install cffi PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage python3 -m pip install coverage
@ -13,8 +14,7 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
# TODO Remove condition when NumPy supports 3.12 python3 -m pip install numpy
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; 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

@ -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:
@ -36,7 +44,7 @@ jobs:
git config --global core.autocrlf input git config --global core.autocrlf input
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v4 uses: cygwin/cygwin-install-action@v4
@ -76,17 +84,23 @@ jobs:
with: with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
- name: Select Python version
run: |
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
- name: Get latest NumPy version
id: latest-numpy
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
- name: pip cache - name: pip cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: 'C:\cygwin\home\runneradmin\.cache\pip' path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
restore-keys: | restore-keys: |
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-
- name: Select Python version
run: |
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
- name: Build system information - name: Build system information
run: | run: |
@ -96,10 +110,10 @@ jobs:
run: | run: |
bash.exe .ci/install.sh bash.exe .ci/install.sh
- name: Install a different NumPy - name: Upgrade NumPy
shell: dash.exe -l "{0}" shell: dash.exe -l "{0}"
run: | run: |
python3 -m pip install -U numpy python3 -m pip install -U "numpy<1.26"
- name: Build - name: Build
shell: bash.exe -eo pipefail -o igncr "{0}" shell: bash.exe -eo pipefail -o igncr "{0}"

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:
@ -59,7 +67,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

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:
@ -34,7 +42,7 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up shell - name: Set up shell
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH

View File

@ -37,7 +37,7 @@ jobs:
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py

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"]
timeout-minutes: 30 timeout-minutes: 30
@ -32,16 +40,16 @@ jobs:
steps: steps:
- name: Checkout Pillow - name: Checkout Pillow
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Checkout cached dependencies - name: Checkout cached dependencies
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: python-pillow/pillow-depends repository: python-pillow/pillow-depends
path: winbuild\depends path: winbuild\depends
- name: Checkout extra test images - name: Checkout extra test images
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: python-pillow/test-images repository: python-pillow/test-images
path: Tests\test-images path: Tests\test-images

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:
@ -31,7 +39,7 @@ jobs:
python-version: [ python-version: [
"pypy3.10", "pypy3.10",
"pypy3.9", "pypy3.9",
"3.12-dev", "3.12",
"3.11", "3.11",
"3.10", "3.10",
"3.9", "3.9",
@ -48,7 +56,7 @@ jobs:
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4

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

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

@ -0,0 +1,42 @@
name: Wheels
on:
push:
paths:
- ".github/workflows/wheels*.yml"
- "wheels/*"
tags:
- "*"
pull_request:
paths:
- ".github/workflows/wheels*.yml"
- "wheels/*"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
macos:
uses: ./.github/workflows/wheels-macos.yml
with:
artifacts-name: "wheels"
linux:
uses: ./.github/workflows/wheels-linux.yml
with:
artifacts-name: "wheels"
success:
permissions:
contents: none
needs: [macos, linux]
runs-on: ubuntu-latest
name: Wheels Successful
steps:
- name: Success
run: echo Wheels Successful

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,6 +1,12 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/asottile/pyupgrade
rev: 23.3.0 rev: v3.13.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.9.1
hooks: hooks:
- id: black - id: black
args: [--target-version=py38] args: [--target-version=py38]
@ -23,17 +29,17 @@ repos:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.1 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 - repo: https://github.com/PyCQA/flake8
rev: 6.0.0 rev: 6.1.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat] [flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0 rev: v1.10.0
@ -44,23 +50,28 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.4.0
hooks: hooks:
- id: check-executables-have-shebangs
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json - id: check-json
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer
exclude: ^Tests/images/
- id: trailing-whitespace
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6.7 rev: v0.6.8
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: 0.12.1 rev: 1.2.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.13 rev: v0.14
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

135
.travis.yml Normal file
View File

@ -0,0 +1,135 @@
if: tag IS present
env:
global:
- CONFIG_PATH=wheels/config.sh
- REPO_DIR=.
- PLAT=aarch64
- TEST_DEPENDS=pytest-timeout
language: python
# Default Python version is usually 3.6
python: "3.11"
dist: focal
services: docker
jobs:
include:
- name: "3.8 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.8
- name: "3.8 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.8
- name: "3.8 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.8
- name: "3.9 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.9
- name: "3.9 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.9
- name: "3.9 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.9
- name: "3.10 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.10
- name: "3.10 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.10
- name: "3.10 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.10
- name: "3.11 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.11
- name: "3.11 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.11
- name: "3.11 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.11
- name: "3.12 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.12
- name: "3.12 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.12
- name: "3.12 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.12
before_install:
- source wheels/multibuild/common_utils.sh
- source wheels/multibuild/travis_steps.sh
- before_install
install:
- build_multilinux aarch64 build_wheel
- ls -l "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/"
script:
- install_run
# Upload wheels to GitHub Releases
deploy:
provider: releases
api_key: $GITHUB_RELEASE_TOKEN
file_glob: true
file: "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/*.whl"
on:
repo: python-pillow/Pillow
tags: true
skip_cleanup: true

View File

@ -5,9 +5,90 @@ Changelog (Pillow)
10.1.0 (unreleased) 10.1.0 (unreleased)
------------------- -------------------
- Allow saving I;16B images as PNG #7302
[radarhere]
- Corrected drawing I;16 points and writing I;16 text #7257
[radarhere]
- Set blue channel to 128 for BC5S #7413
[radarhere]
- Increase flexibility when reading IPTC fields #7319
[radarhere]
- Set C palette to be empty by default #7289
[radarhere]
- Added gs_binary to control Ghostscript use on all platforms #7392
[radarhere]
- Read bounding box information from the trailer of EPS files if specified #7382
[nopperl, radarhere]
- Added reading 8-bit color DDS images #7426
[radarhere]
- Added has_transparency_data #7420
[radarhere, hugovk]
- Fixed bug when reading BC5S DDS images #7401
[radarhere]
- Prevent TIFF orientation from being applied more than once #7383
[radarhere]
- Use previous pixel alpha for QOI_OP_RGB #7357
[radarhere]
- Added BC5U reading #7358
[radarhere]
- Allow getpixel() to accept a list #7355
[radarhere, homm]
- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336
[radarhere]
- Expand JPEG buffer size when saving optimized or progressive #7345
[radarhere]
- Added session type check for Linux in ImageGrab.grabclipboard() #7332
[TheNooB2706, radarhere, hugovk]
- Allow "loop=None" when saving GIF images #7329
[radarhere]
- Fixed transparency when saving P mode images to PDF #7323
[radarhere]
- Added saving LA images as PDFs #7299
[radarhere]
- Set SMaskInData to 1 for PDFs with alpha #7316, #7317
[radarhere]
- Changed Image mode property to be read-only by default #7307
[radarhere]
- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266
[mtreinish, radarhere]
- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284
[radarhere]
- Fix missing symbols when libtiff depends on libjpeg #7270 - Fix missing symbols when libtiff depends on libjpeg #7270
[heitbaum] [heitbaum]
10.0.1 (2023-09-15)
-------------------
- Updated libwebp to 1.3.2 #7395
[radarhere]
- Updated zlib to 1.3 #7344
[radarhere]
10.0.0 (2023-07-01) 10.0.0 (2023-07-01)
------------------- -------------------
@ -5735,8 +5816,8 @@ http://svn.effbot.org/public/pil/
a polyline, independent of line angle. a polyline, independent of line angle.
- Fixed bearing calculation and clipping in the ImageFont truetype - Fixed bearing calculation and clipping in the ImageFont truetype
renderer; this could lead to clipped text, or crashes in the low- renderer; this could lead to clipped text, or crashes in the low-level
level _imagingft module. (based on input from Adam Twardoch and _imagingft module. (based on input from Adam Twardoch and
others). others).
- Added ImageQt wrapper module, for converting PIL Image objects to - Added ImageQt wrapper module, for converting PIL Image objects to
@ -5817,8 +5898,7 @@ http://svn.effbot.org/public/pil/
1.1.5c2 and 1.1.5 final 1.1.5c2 and 1.1.5 final
----------------------- -----------------------
- Added experimental PERSPECTIVE transform method (from Jeff Breiden- - Added experimental PERSPECTIVE transform method (from Jeff Breidenbach).
bach).
1.1.5c1 1.1.5c1
------- -------
@ -5890,8 +5970,8 @@ http://svn.effbot.org/public/pil/
- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA". - Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA".
- Added "getcolors()" method. This is similar to the existing histo- - Added "getcolors()" method. This is similar to the existing histogram
gram method, but looks at color values instead of individual layers, method, but looks at color values instead of individual layers,
and returns an unsorted list of (count, color) tuples. and returns an unsorted list of (count, color) tuples.
By default, the method returns None if finds more than 256 colors. By default, the method returns None if finds more than 256 colors.
@ -6107,8 +6187,8 @@ http://svn.effbot.org/public/pil/
- Added limited support for "bitfield compression" in BMP files - Added limited support for "bitfield compression" in BMP files
and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This
also fixes a problem with ImageGrab module when copying screen- also fixes a problem with ImageGrab module when copying screendumps
dumps from the clipboard on 15/16/32-bit displays. from the clipboard on 15/16/32-bit displays.
- Added experimental WAL (Quake 2 textures) loader. To use this - Added experimental WAL (Quake 2 textures) loader. To use this
loader, import WalImageFile and call the "open" method in that loader, import WalImageFile and call the "open" method in that
@ -6219,8 +6299,8 @@ http://svn.effbot.org/public/pil/
1.1.3 final 1.1.3 final
----------- -----------
- Made setup.py look for old versions of zlib. For some back- - Made setup.py look for old versions of zlib. For some background,
ground, see: https://zlib.net/advisory-2002-03-11.txt see: https://zlib.net/advisory-2002-03-11.txt
1.1.3c2 1.1.3c2
------- -------
@ -6411,8 +6491,8 @@ http://svn.effbot.org/public/pil/
supports all major PIL image modes (including F and I). supports all major PIL image modes (including F and I).
- The ImageFile module now includes a Parser class, which can - The ImageFile module now includes a Parser class, which can
be used to incrementally decode an image file (while down- be used to incrementally decode an image file (while downloading
loading it from the net, for example). See the handbook for it from the net, for example). See the handbook for
details. details.
- "show" now converts non-standard modes to "L" or "RGB" (as - "show" now converts non-standard modes to "L" or "RGB" (as
@ -6550,8 +6630,8 @@ http://svn.effbot.org/public/pil/
- The Image "transform" method now supports Image.QUAD transforms. - The Image "transform" method now supports Image.QUAD transforms.
The data argument is an 8-tuple giving the upper left, lower The data argument is an 8-tuple giving the upper left, lower
left, lower right, and upper right corner of the source quadri- left, lower right, and upper right corner of the source quadrilateral.
lateral. Also added Image.MESH transform which takes a list Also added Image.MESH transform which takes a list
of quadrilaterals. of quadrilaterals.
- The Image "resize", "rotate", and "transform" methods now support - The Image "resize", "rotate", and "transform" methods now support
@ -6776,8 +6856,8 @@ The test suite includes 400 individual tests.
neither "short", "int" nor "long" are 32-bit wide. neither "short", "int" nor "long" are 32-bit wide.
- Added file= and data= keyword arguments to PhotoImage and BitmapImage. - Added file= and data= keyword arguments to PhotoImage and BitmapImage.
This allows you to use them as drop-in replacements for the corre- This allows you to use them as drop-in replacements for the corresponding
sponding Tkinter classes. Tkinter classes.
- Removed bogus references to the crack coder (ImagingCrack). - Removed bogus references to the crack coder (ImagingCrack).

View File

@ -29,3 +29,4 @@ global-exclude .git*
global-exclude *.pyc global-exclude *.pyc
global-exclude *.so global-exclude *.so
prune .ci prune .ci
prune wheels

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>

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.
@ -99,17 +99,14 @@ Released as needed privately to individual vendors for critical security-related
## Binary Distributions ## Binary Distributions
### macOS and Linux ### macOS and Linux
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): * [ ] Download wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash ```bash
git clone https://github.com/python-pillow/pillow-wheels gh run download --dir dist
cd pillow-wheels # select dist-x.y.z
./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)

0
Tests/check_j2k_leaks.py Executable file → Normal file
View File

View File

@ -91,7 +91,7 @@ def assert_image_equal(a, b, msg=None):
if HAS_UPLOADER: if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = test_image_results.upload(a, b)
logger.error(f"Url for test images: {url}") logger.error("URL for test images: %s", url)
except Exception: except Exception:
pass pass
@ -126,7 +126,7 @@ def assert_image_similar(a, b, epsilon, msg=None):
if HAS_UPLOADER: if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = test_image_results.upload(a, b)
logger.error(f"Url for test images: {url}") logger.exception("URL for test images: %s", url)
except Exception: except Exception:
pass pass
raise e raise e

View File

@ -22,4 +22,3 @@ and that the name of ICC shall not be used in advertising or publicity
pertaining to distribution of the software without specific, written pertaining to distribution of the software without specific, written
prior permission. ICC makes no representations about the suitability prior permission. ICC makes no representations about the suitability
of this software for any purpose. of this software for any purpose.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 95 KiB

BIN
Tests/images/bc5u.dds Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 180 B

0
Tests/images/negative_size.ppm Executable file → Normal file
View File

BIN
Tests/images/palette.dds Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,6 +6,7 @@ import packaging
import pytest import pytest
from PIL import Image, features from PIL import Image, features
from Tests.helper import skip_unless_feature
if sys.platform.startswith("win32"): if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True) pytest.skip("Fuzzer is linux only", allow_module_level=True)
@ -48,6 +49,7 @@ def test_fuzz_images(path):
fuzzers.disable_decompressionbomb_error() fuzzers.disable_decompressionbomb_error()
@skip_unless_feature("freetype2")
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
) )

View File

@ -22,7 +22,7 @@ def test_imageops_box_blur():
def box_blur(image, radius=1, n=1): def box_blur(image, radius=1, n=1):
return image._new(image.im.box_blur(radius, n)) return image._new(image.im.box_blur((radius, radius), n))
def assert_image(im, data, delta=0): def assert_image(im, data, delta=0):

View File

@ -16,6 +16,7 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds" TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
TEST_FILE_BC6H = "Tests/images/bc6h.dds" TEST_FILE_BC6H = "Tests/images/bc6h.dds"
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds" TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
@ -81,10 +82,18 @@ def test_sanity_ati1():
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
def test_sanity_ati2(): @pytest.mark.parametrize(
"""Check ATI2 images can be opened""" "image_path",
(
TEST_FILE_ATI2,
# hexeditted to use BC5U FourCC
TEST_FILE_BC5U,
),
)
def test_sanity_ati2_bc5u(image_path):
"""Check ATI2 and BC5U images can be opened"""
with Image.open(TEST_FILE_ATI2) as im: with Image.open(image_path) as im:
im.load() im.load()
assert im.format == "DDS" assert im.format == "DDS"
@ -289,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
@ -875,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
assert reread.info["duration"] == 8500 assert reread.info["duration"] == 8500
def test_loop_none(tmp_path):
out = str(tmp_path / "temp.gif")
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=None)
with Image.open(out) as reread:
assert "loop" not in reread.info
def test_number_of_loops(tmp_path): def test_number_of_loops(tmp_path):
number_of_loops = 2 number_of_loops = 2
@ -1086,6 +1094,21 @@ def test_transparent_optimize(tmp_path):
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
def test_removed_transparency(tmp_path):
out = str(tmp_path / "temp.gif")
im = Image.new("RGB", (256, 1))
for x in range(256):
im.putpixel((x, 0), (x, 0, 0))
im.info["transparency"] = (255, 255, 255)
with pytest.warns(UserWarning):
im.save(out)
with Image.open(out) as reloaded:
assert "transparency" not in reloaded.info
def test_rgb_transparency(tmp_path): def test_rgb_transparency(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
@ -1157,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,5 @@
import sys import sys
from io import StringIO from io import BytesIO, StringIO
from PIL import Image, IptcImagePlugin from PIL import Image, IptcImagePlugin
@ -30,6 +30,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
assert False, "FotoStation tag not found"
def test_getiptcinfo_zero_padding():
# Arrange
with Image.open(TEST_FILE) as im:
im.info["photoshop"][0x0404] += b"\x00\x00\x00"
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert isinstance(iptc, dict)
assert len(iptc) == 3
def test_getiptcinfo_tiff_none(): 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

@ -214,13 +214,20 @@ class TestFileJpeg:
# Should not raise OSError for image with icc larger than image size. # Should not raise OSError for image with icc larger than image size.
im.save( im.save(
f, f,
format="JPEG",
progressive=True, progressive=True,
quality=95, quality=95,
icc_profile=icc_profile, icc_profile=icc_profile,
optimize=True, optimize=True,
) )
with Image.open("Tests/images/flower2.jpg") as im:
f = str(tmp_path / "temp2.jpg")
im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955)
with Image.open("Tests/images/flower2.jpg") as im:
f = str(tmp_path / "temp3.jpg")
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
def test_optimize(self): def test_optimize(self):
im1 = self.roundtrip(hopper()) im1 = self.roundtrip(hopper())
im2 = self.roundtrip(hopper(), optimize=0) im2 = self.roundtrip(hopper(), optimize=0)
@ -945,11 +952,10 @@ class TestFileJpeg:
assert repr_jpeg.format == "JPEG" assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17) assert_image_similar(im, repr_jpeg, 17)
def test_repr_jpeg_error(self): def test_repr_jpeg_error_returns_none(self):
im = hopper("F") im = hopper("F")
with pytest.raises(ValueError): assert im._repr_jpeg_() is None
im._repr_jpeg_()
@pytest.mark.skipif(not is_win32(), reason="Windows only") @pytest.mark.skipif(not is_win32(), reason="Windows only")

View File

@ -274,17 +274,15 @@ def test_sgnd(tmp_path):
assert reloaded_signed.getpixel((0, 0)) == 128 assert reloaded_signed.getpixel((0, 0)) == 128
def test_rgba(): @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_rgba(ext):
# Arrange # Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: # Act
# Act im.load()
j2k.load()
jp2.load()
# Assert # Assert
assert j2k.mode == "RGBA" assert im.mode == "RGBA"
assert jp2.mode == "RGBA"
@pytest.mark.parametrize("ext", (".j2k", ".jp2")) @pytest.mark.parametrize("ext", (".j2k", ".jp2"))

View File

@ -8,7 +8,7 @@ from collections import namedtuple
import pytest import pytest
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
from .helper import ( from .helper import (
@ -1035,7 +1035,18 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im: with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9): for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
assert 274 in im.tag_v2
im.load() im.load()
assert 274 not in im.tag_v2
assert_image_similar(base_im, im, 0.7)
def test_exif_transpose(self):
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
for i in range(2, 9):
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
im = ImageOps.exif_transpose(im)
assert_image_similar(base_im, im, 0.7) assert_image_similar(base_im, im, 0.7)

View File

@ -43,8 +43,25 @@ def test_save(tmp_path, mode):
@skip_unless_feature("jpg_2000") @skip_unless_feature("jpg_2000")
def test_save_rgba(tmp_path): @pytest.mark.parametrize("mode", ("LA", "RGBA"))
helper_save_as_pdf(tmp_path, "RGBA") def test_save_alpha(tmp_path, mode):
helper_save_as_pdf(tmp_path, mode)
def test_p_alpha(tmp_path):
# Arrange
outfile = str(tmp_path / "temp.pdf")
with Image.open("Tests/images/pil123p.png") as im:
assert im.mode == "P"
assert isinstance(im.info["transparency"], bytes)
# Act
im.save(outfile)
# Assert
with open(outfile, "rb") as fp:
contents = fp.read()
assert b"\n/SMask " in contents
def test_monochrome(tmp_path): def test_monochrome(tmp_path):
@ -57,8 +74,8 @@ def test_monochrome(tmp_path):
def test_unsupported_mode(tmp_path): def test_unsupported_mode(tmp_path):
im = hopper("LA") im = hopper("PA")
outfile = str(tmp_path / "temp_LA.pdf") outfile = str(tmp_path / "temp_PA.pdf")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.save(outfile) im.save(outfile)

View File

@ -79,7 +79,7 @@ class TestFilePng:
def test_sanity(self, tmp_path): def test_sanity(self, tmp_path):
# internal version number # internal version number
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
@ -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)
@ -532,11 +532,10 @@ class TestFilePng:
assert repr_png.format == "PNG" assert repr_png.format == "PNG"
assert_image_equal(im, repr_png) assert_image_equal(im, repr_png)
def test_repr_png_error(self): def test_repr_png_error_returns_none(self):
im = hopper("F") im = hopper("F")
with pytest.raises(ValueError): assert im._repr_png_() is None
im._repr_png_()
def test_chunk_order(self, tmp_path): def test_chunk_order(self, tmp_path):
with Image.open("Tests/images/icc_profile.png") as im: with Image.open("Tests/images/icc_profile.png") as im:

View File

@ -2,7 +2,7 @@ import pytest
from PIL import Image, QoiImagePlugin from PIL import Image, QoiImagePlugin
from .helper import assert_image_equal_tofile, assert_image_similar_tofile from .helper import assert_image_equal_tofile
def test_sanity(): def test_sanity():
@ -18,7 +18,7 @@ def test_sanity():
assert im.size == (162, 150) assert im.size == (162, 150)
assert im.format == "QOI" assert im.format == "QOI"
assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03) assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
def test_invalid_file(): def test_invalid_file():

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

@ -135,6 +135,12 @@ class TestImage:
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
im.size = (3, 4) im.size = (3, 4)
def test_set_mode(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(AttributeError):
im.mode = "P"
def test_invalid_image(self): def test_invalid_image(self):
im = io.BytesIO(b"") im = io.BytesIO(b"")
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):
@ -632,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])
@ -655,15 +661,15 @@ class TestImage:
blank_p.palette = None blank_p.palette = None
blank_pa.palette = None blank_pa.palette = None
def _make_new(base_image, im, palette_result=None): def _make_new(base_image, image, palette_result=None):
new_im = base_image._new(im) new_image = base_image._new(image.im)
assert new_im.mode == im.mode assert new_image.mode == image.mode
assert new_im.size == im.size assert new_image.size == image.size
assert new_im.info == base_image.info assert new_image.info == base_image.info
if palette_result is not None: if palette_result is not None:
assert new_im.palette.tobytes() == palette_result.tobytes() assert new_image.palette.tobytes() == palette_result.tobytes()
else: else:
assert new_im.palette is None assert new_image.palette is None
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im_p, im, None) _make_new(im_p, im, None)
@ -900,6 +906,31 @@ class TestImage:
im = Image.new("RGB", size) im = Image.new("RGB", size)
assert im.tobytes() == b"" assert im.tobytes() == b""
def test_has_transparency_data(self):
for mode in ("1", "L", "P", "RGB"):
im = Image.new(mode, (1, 1))
assert not im.has_transparency_data
for mode in ("LA", "La", "PA", "RGBA", "RGBa"):
im = Image.new(mode, (1, 1))
assert im.has_transparency_data
# P mode with "transparency" info
with Image.open("Tests/images/first_frame_transparency.gif") as im:
assert "transparency" in im.info
assert im.has_transparency_data
# RGB mode with "transparency" info
with Image.open("Tests/images/rgb_trns.png") as im:
assert "transparency" in im.info
assert im.has_transparency_data
# P mode with RGBA palette
im = Image.new("RGBA", (1, 1)).convert("P")
assert im.mode == "P"
assert im.palette.mode == "RGBA"
assert im.has_transparency_data
def test_apply_transparency(self): 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))

View File

@ -213,6 +213,10 @@ class TestImageGetPixel(AccessTest):
def test_basic(self, mode): def test_basic(self, mode):
self.check(mode) self.check(mode)
def test_list(self):
im = hopper()
assert im.getpixel([0, 0]) == (20, 20, 70)
@pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize( @pytest.mark.parametrize(
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)

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

@ -23,9 +23,12 @@ from .helper import assert_image_equal, hopper
ImageFilter.MinFilter, ImageFilter.MinFilter,
ImageFilter.ModeFilter, ImageFilter.ModeFilter,
ImageFilter.GaussianBlur, ImageFilter.GaussianBlur,
ImageFilter.GaussianBlur(0),
ImageFilter.GaussianBlur(5), ImageFilter.GaussianBlur(5),
ImageFilter.GaussianBlur((2, 5)),
ImageFilter.BoxBlur(0), ImageFilter.BoxBlur(0),
ImageFilter.BoxBlur(5), ImageFilter.BoxBlur(5),
ImageFilter.BoxBlur((2, 5)),
ImageFilter.UnsharpMask, ImageFilter.UnsharpMask,
ImageFilter.UnsharpMask(10), ImageFilter.UnsharpMask(10),
), ),
@ -185,12 +188,21 @@ def test_consistency_5x5(mode):
assert_image_equal(source.filter(kernel), reference) assert_image_equal(source.filter(kernel), reference)
def test_invalid_box_blur_filter(): @pytest.mark.parametrize(
"radius",
(
-2,
(-2, -2),
(-2, 2),
(2, -2),
),
)
def test_invalid_box_blur_filter(radius):
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFilter.BoxBlur(-2) ImageFilter.BoxBlur(radius)
im = hopper() im = hopper()
box_blur_filter = ImageFilter.BoxBlur(2) box_blur_filter = ImageFilter.BoxBlur(2)
box_blur_filter.radius = -2 box_blur_filter.radius = radius
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.filter(box_blur_filter) im.filter(box_blur_filter)

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

@ -586,6 +586,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 +744,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")
@ -1326,6 +1338,7 @@ def test_stroke_multiline():
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3)
@skip_unless_feature("freetype2")
def test_setting_default_font(): def test_setting_default_font():
# Arrange # Arrange
im = Image.new("RGB", (100, 250)) im = Image.new("RGB", (100, 250))

View File

@ -136,7 +136,7 @@ class TestImageFile:
class DummyImageFile(ImageFile.ImageFile): class DummyImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
self.mode = "RGB" self._mode = "RGB"
self._size = (1, 1) self._size = (1, 1)
im = DummyImageFile(buf) im = DummyImageFile(buf)
@ -217,7 +217,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100
class MockImageFile(ImageFile.ImageFile): class MockImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
self.rawmode = "RGBA" self.rawmode = "RGBA"
self.mode = "RGBA" self._mode = "RGBA"
self._size = (200, 200) self._size = (200, 200)
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]

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)

View File

@ -75,13 +75,13 @@ def test_pickle_la_mode_with_palette(tmp_path):
# Act / Assert # Act / Assert
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
im.mode = "LA" im._mode = "LA"
with open(filename, "wb") as f: with open(filename, "wb") as f:
pickle.dump(im, f, protocol) pickle.dump(im, f, protocol)
with open(filename, "rb") as f: with open(filename, "rb") as f:
loaded_im = pickle.load(f) loaded_im = pickle.load(f)
im.mode = "PA" im._mode = "PA"
assert im == loaded_im assert im == loaded_im
@ -112,6 +112,7 @@ def helper_assert_pickled_font_images(font1, font2):
assert_image_equal(im1, im2) assert_image_equal(im1, im2)
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_string(protocol): def test_pickle_font_string(protocol):
# Arrange # Arrange
@ -125,6 +126,7 @@ def test_pickle_font_string(protocol):
helper_assert_pickled_font_images(font, unpickled_font) helper_assert_pickled_font_images(font, unpickled_font)
@skip_unless_feature("freetype2")
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
def test_pickle_font_file(tmp_path, protocol): def test_pickle_font_file(tmp_path, protocol):
# Arrange # Arrange

0
_custom_build/backend.py Executable file → Normal file
View File

View File

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

View File

@ -11,4 +11,3 @@ pushd $archive
meson build --prefix=/usr && sudo ninja -C build install meson build --prefix=/usr && sudo ninja -C build install
popd popd

View File

@ -15,4 +15,3 @@ make && sudo make install
cd .. cd ..
popd popd

View File

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

View File

@ -2,4 +2,3 @@
pkg install -y python ndk-sysroot clang make \ pkg install -y python ndk-sysroot clang make \
libjpeg-turbo libjpeg-turbo

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

@ -225,7 +225,7 @@ class DdsImageFile(ImageFile.ImageFile):
flags, height, width = struct.unpack("<3I", header.read(12)) flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height) self._size = (width, height)
self.mode = "RGBA" self._mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved struct.unpack("<11I", header.read(44)) # reserved

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
@ -253,7 +285,7 @@ their :py:attr:`~PIL.Image.Image.info` values.
**loop** **loop**
Integer number of times the GIF should loop. 0 means that it will loop Integer number of times the GIF should loop. 0 means that it will loop
forever. By default, the image will not loop. forever. If omitted or ``None``, the image will not loop.
**comment** **comment**
A comment about the image. A comment about the image.
@ -861,6 +893,10 @@ PPM
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
``RGB`` data. ``RGB`` data.
"Raw" (P4 to P6) formats can be read, and are used when writing.
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
SGI SGI
^^^ ^^^
@ -1482,7 +1518,7 @@ files. Different encoding methods are used, depending on the image mode.
unavailable unavailable
* L, RGB and CMYK mode images use JPEG encoding * L, RGB and CMYK mode images use JPEG encoding
* P mode images use HEX encoding * P mode images use HEX encoding
* RGBA mode images use JPEG2000 encoding * LA and RGBA mode images use JPEG2000 encoding
.. _pdf-saving: .. _pdf-saving:

View File

@ -72,11 +72,11 @@ true color.
# mode setting # mode setting
bits = int(header[3]) bits = int(header[3])
if bits == 1: if bits == 1:
self.mode = "1" self._mode = "1"
elif bits == 8: elif bits == 8:
self.mode = "L" self._mode = "L"
elif bits == 24: elif bits == 24:
self.mode = "RGB" self._mode = "RGB"
else: else:
msg = "unknown number of bits" msg = "unknown number of bits"
raise SyntaxError(msg) raise SyntaxError(msg)

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

View File

@ -83,10 +83,9 @@ Install Pillow with :command:`pip`::
.. tab:: Windows .. tab:: Windows
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 both 32 and 64-bit versions in the wheel format. supported Pythons in 64-bit versions in the wheel format. These binaries include
These binaries include support for all optional libraries except support for all optional libraries except libimagequant and libxcb. Raqm support
libimagequant and libxcb. Raqm support requires requires FriBiDi to be installed separately::
FriBiDi to be installed separately::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow python3 -m pip install --upgrade Pillow
@ -181,7 +180,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** * Pillow has been tested with libimagequant **2.6-4.2.1**
* 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.
@ -499,11 +498,13 @@ These platforms have been reported to work at the versions mentioned.
| 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.0 |arm | | macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +---------------------------+------------------+ |
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
| +---------------------------+------------------+ |
| | 3.7 | 9.5.0 | |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+--------------+ | +---------------------------+------------------+--------------+

View File

@ -1,7 +1,8 @@
Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
Pillow >= 10,Yes,Yes,Yes,Yes,,, Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,,
Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,, Pillow 10.0,,Yes,Yes,Yes,Yes,,,
Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,,
Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,,
Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes

1 Python 3.12 3.11 3.10 3.9 3.8 3.7 3.6 3.5
2 Pillow >= 10 Pillow >= 10.1 Yes Yes Yes Yes Yes
3 Pillow 9.3 - 9.5 Pillow 10.0 Yes Yes Yes Yes Yes
4 Pillow 9.0 - 9.2 Pillow 9.3 - 9.5 Yes Yes Yes Yes Yes
5 Pillow 8.3.2 - 8.4 Pillow 9.0 - 9.2 Yes Yes Yes Yes Yes
6 Pillow 8.0 - 8.3.1 Pillow 8.3.2 - 8.4 Yes Yes Yes Yes Yes
7 Pillow 7.0 - 7.2 Pillow 8.0 - 8.3.1 Yes Yes Yes Yes Yes
8 Pillow 7.0 - 7.2 Yes Yes Yes Yes

View File

@ -93,10 +93,14 @@ Generating images
Registering plugins Registering plugins
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
.. autofunction:: preinit
.. autofunction:: init
.. note:: .. note::
These functions are for use by plugin authors. Application authors can These functions are for use by plugin authors. They are called when a
ignore them. plugin is loaded as part of :py:meth:`~preinit()` or :py:meth:`~init()`.
Application authors can ignore them.
.. autofunction:: register_open .. autofunction:: register_open
.. autofunction:: register_mime .. autofunction:: register_mime
@ -347,6 +351,8 @@ Instances of the :py:class:`Image` class have the following attributes:
.. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell` .. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell`
.. autoattribute:: PIL.Image.Image.has_transparency_data
Classes Classes
------- -------

View File

@ -538,7 +538,7 @@ 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).
:return: Width for horizontal, height for vertical text. :return: Either width for horizontal text, or height for vertical text.
.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) .. 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)

View File

@ -206,4 +206,4 @@ Support reading signed 8-bit TIFF images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TIFF images with signed integer data, 8 bits per sample and a photometric TIFF images with signed integer data, 8 bits per sample and a photometric
interpretaton of BlackIsZero can now be read. interpretation of BlackIsZero can now be read.

View File

@ -0,0 +1,14 @@
10.0.1
------
Security
========
This release addresses :cve:`2023-4863`, by providing an updated install script and
updated wheels to include libwebp 1.3.2, preventing a potential heap buffer overflow
in WebP.
Updated tests to pass with latest zlib version
==============================================
The release of zlib 1.3 caused one of the tests in the Pillow test suite to fail.

View File

@ -0,0 +1,66 @@
10.1.0
------
Backwards Incompatible Changes
==============================
Setting image mode
^^^^^^^^^^^^^^^^^^
If you attempt to set the mode of an image directly, e.g.
``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is
not about removing existing functionality, but instead about raising an
explicit error to prevent later consequences. The ``convert`` method is the
correct way to change an image's mode.
Deprecations
============
TODO
^^^^
TODO
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
has_transparency_data
^^^^^^^^^^^^^^^^^^^^^
Images now have :py:attr:`~PIL.Image.Image.has_transparency_data` to indicate
whether the image has transparency data, whether in the form of an alpha
channel, a palette with an alpha channel, or a "transparency" key in the
:py:attr:`~PIL.Image.Image.info` dictionary.
Even if this attribute is true, the image might still appear solid, if all of
the values shown within are opaque.
Security
========
TODO
^^^^
TODO
Other Changes
=============
Added support for DDS 8-bit color indexed images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support has been added to read PALETTEINDEXED8 DDS files as P mode images.
Support reading signed 8-bit YCbCr TIFF images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TIFF images with unsigned integer data, 8 bits per sample and a photometric
interpretation of YCbCr can now be read.

View File

@ -49,4 +49,3 @@ The external dependencies on libjpeg and zlib are now required by default.
If the headers or libraries are not found, then installation will abort If the headers or libraries are not found, then installation will abort
with an error. This behaviour can be disabled with the ``--disable-libjpeg`` with an error. This behaviour can be disabled with the ``--disable-libjpeg``
and ``--disable-zlib`` flags. and ``--disable-zlib`` flags.

View File

@ -34,7 +34,3 @@ image size can lead to a smaller allocation than expected, leading to
arbitrary writes. arbitrary writes.
This issue was found by Cris Neckar at Divergent Security. This issue was found by Cris Neckar at Divergent Security.

View File

@ -20,5 +20,3 @@ CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix
undefines PySlice_GetIndicesEx if it exists to restore compatibility undefines PySlice_GetIndicesEx if it exists to restore compatibility
with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for
more details. more details.

View File

@ -8,4 +8,3 @@ Fixed Windows PyPy Build
A change in the 4.2.0 cycle broke the Windows PyPy build. This has A change in the 4.2.0 cycle broke the Windows PyPy build. This has
been fixed, and PyPy is now part of the Windows CI matrix. been fixed, and PyPy is now part of the Windows CI matrix.

View File

@ -175,6 +175,3 @@ Dark theme for docs
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.

View File

@ -14,6 +14,8 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
10.1.0
10.0.1
10.0.0 10.0.0
9.5.0 9.5.0
9.4.0 9.4.0

View File

@ -16,6 +16,7 @@ classifiers =
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python :: Implementation :: PyPy
Topic :: Multimedia :: Graphics Topic :: Multimedia :: Graphics

View File

@ -39,7 +39,7 @@ TIFF_ROOT = None
ZLIB_ROOT = None ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
if sys.platform == "win32" and sys.version_info >= (3, 12): if sys.platform == "win32" and sys.version_info >= (3, 13):
import atexit import atexit
atexit.register( atexit.register(

View File

@ -68,11 +68,11 @@ def bdf_char(f):
# followed by the width in x (BBw), height in y (BBh), # followed by the width in x (BBw), height in y (BBh),
# and x and y displacement (BBxoff0, BByoff0) # and x and y displacement (BBxoff0, BByoff0)
# of the lower left corner from the origin of the character. # of the lower left corner from the origin of the character.
width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
# The word DWIDTH # The word DWIDTH
# followed by the width in x and y of the character in device pixels. # followed by the width in x and y of the character in device pixels.
dwx, dwy = [int(p) for p in props["DWIDTH"].split()] dwx, dwy = (int(p) for p in props["DWIDTH"].split())
bbox = ( bbox = (
(dwx, dwy), (dwx, dwy),

View File

@ -266,7 +266,7 @@ class BlpImageFile(ImageFile.ImageFile):
msg = f"Bad BLP magic {repr(self.magic)}" msg = f"Bad BLP magic {repr(self.magic)}"
raise BLPFormatError(msg) raise BLPFormatError(msg)
self.mode = "RGBA" if self._blp_alpha_depth else "RGB" self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
@ -419,9 +419,11 @@ class BLPEncoder(ImageFile.PyEncoder):
def _write_palette(self): def _write_palette(self):
data = b"" data = b""
palette = self.im.getpalette("RGBA", "RGBA") palette = self.im.getpalette("RGBA", "RGBA")
for i in range(256): for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4] r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a) data += struct.pack("<4B", b, g, r, a)
while len(data) < 256 * 4:
data += b"\x00" * 4
return data return data
def encode(self, bufsize): def encode(self, bufsize):
@ -442,7 +444,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data return len(data), 0, data
def _save(im, fp, filename, save_all=False): def _save(im, fp, filename):
if im.mode != "P": if im.mode != "P":
msg = "Unsupported BLP image mode" msg = "Unsupported BLP image mode"
raise ValueError(msg) raise ValueError(msg)

View File

@ -163,7 +163,7 @@ class BmpImageFile(ImageFile.ImageFile):
offset += 4 * file_info["colors"] offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values # ---------------------- Check bit depth for unusual unsupported values
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None: if self.mode is None:
msg = f"Unsupported BMP pixel depth ({file_info['bits']})" msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
raise OSError(msg) raise OSError(msg)
@ -200,7 +200,7 @@ class BmpImageFile(ImageFile.ImageFile):
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
): ):
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
self.mode = "RGBA" if "A" in raw_mode else self.mode self._mode = "RGBA" if "A" in raw_mode else self.mode
elif ( elif (
file_info["bits"] in (24, 16) file_info["bits"] in (24, 16)
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
@ -214,7 +214,7 @@ class BmpImageFile(ImageFile.ImageFile):
raise OSError(msg) raise OSError(msg)
elif file_info["compression"] == self.RAW: elif file_info["compression"] == self.RAW:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self.mode = "BGRA", "RGBA" raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (self.RLE8, self.RLE4): elif file_info["compression"] in (self.RLE8, self.RLE4):
decoder_name = "bmp_rle" decoder_name = "bmp_rle"
else: else:
@ -245,10 +245,10 @@ class BmpImageFile(ImageFile.ImageFile):
# ------- If all colors are grey, white or black, ditch palette # ------- If all colors are grey, white or black, ditch palette
if greyscale: if greyscale:
self.mode = "1" if file_info["colors"] == 2 else "L" self._mode = "1" if file_info["colors"] == 2 else "L"
raw_mode = self.mode raw_mode = self.mode
else: else:
self.mode = "P" self._mode = "P"
self.palette = ImagePalette.raw( self.palette = ImagePalette.raw(
"BGRX" if padding == 4 else "BGR", palette "BGRX" if padding == 4 else "BGR", palette
) )

View File

@ -46,7 +46,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
self.fp.seek(offset) self.fp.seek(offset)
# make something up # make something up
self.mode = "F" self._mode = "F"
self._size = 1, 1 self._size = 1, 1
loader = self._load() loader = self._load()

View File

@ -13,7 +13,7 @@ Full text of the CC0 license:
import struct import struct
from io import BytesIO from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile, ImagePalette
from ._binary import o32le as o32 from ._binary import o32le as o32
# Magic ("DDS ") # Magic ("DDS ")
@ -128,7 +128,7 @@ class DdsImageFile(ImageFile.ImageFile):
flags, height, width = struct.unpack("<3I", header.read(12)) flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height) self._size = (width, height)
self.mode = "RGBA" self._mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved struct.unpack("<11I", header.read(44)) # reserved
@ -141,9 +141,9 @@ class DdsImageFile(ImageFile.ImageFile):
if pfflags & DDPF_LUMINANCE: if pfflags & DDPF_LUMINANCE:
# Texture contains uncompressed L or LA data # Texture contains uncompressed L or LA data
if pfflags & DDPF_ALPHAPIXELS: if pfflags & DDPF_ALPHAPIXELS:
self.mode = "LA" self._mode = "LA"
else: else:
self.mode = "L" self._mode = "L"
self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))]
elif pfflags & DDPF_RGB: elif pfflags & DDPF_RGB:
@ -153,10 +153,14 @@ class DdsImageFile(ImageFile.ImageFile):
if pfflags & DDPF_ALPHAPIXELS: if pfflags & DDPF_ALPHAPIXELS:
rawmode += masks[0xFF000000] rawmode += masks[0xFF000000]
else: else:
self.mode = "RGB" self._mode = "RGB"
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF] rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))] self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
elif pfflags & DDPF_PALETTEINDEXED8:
self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
self.tile = [("raw", (0, 0) + self.size, 0, "L")]
else: else:
data_start = header_size + 4 data_start = header_size + 4
n = 0 n = 0
@ -172,15 +176,15 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"ATI1": elif fourcc == b"ATI1":
self.pixel_format = "BC4" self.pixel_format = "BC4"
n = 4 n = 4
self.mode = "L" self._mode = "L"
elif fourcc == b"ATI2": elif fourcc in (b"ATI2", b"BC5U"):
self.pixel_format = "BC5" self.pixel_format = "BC5"
n = 5 n = 5
self.mode = "RGB" self._mode = "RGB"
elif fourcc == b"BC5S": elif fourcc == b"BC5S":
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5
self.mode = "RGB" self._mode = "RGB"
elif fourcc == b"DX10": elif fourcc == b"DX10":
data_start += 20 data_start += 20
# ignoring flags which pertain to volume textures and cubemaps # ignoring flags which pertain to volume textures and cubemaps
@ -189,19 +193,19 @@ class DdsImageFile(ImageFile.ImageFile):
if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM): if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
self.pixel_format = "BC5" self.pixel_format = "BC5"
n = 5 n = 5
self.mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC5_SNORM: elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5
self.mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_UF16: elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
self.pixel_format = "BC6H" self.pixel_format = "BC6H"
n = 6 n = 6
self.mode = "RGB" self._mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_SF16: elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
self.pixel_format = "BC6HS" self.pixel_format = "BC6HS"
n = 6 n = 6
self.mode = "RGB" self._mode = "RGB"
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM): elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7" self.pixel_format = "BC7"
n = 7 n = 7

View File

@ -37,33 +37,39 @@ from ._deprecate import deprecate
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
gs_binary = None
gs_windows_binary = None gs_windows_binary = None
if sys.platform.startswith("win"):
import shutil
for binary in ("gswin32c", "gswin64c", "gs"):
if shutil.which(binary) is not None:
gs_windows_binary = binary
break
else:
gs_windows_binary = False
def has_ghostscript(): def has_ghostscript():
if gs_windows_binary: global gs_binary, gs_windows_binary
return True if gs_binary is None:
if not sys.platform.startswith("win"): if sys.platform.startswith("win"):
try: if gs_windows_binary is None:
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL) import shutil
return True
except OSError: for binary in ("gswin32c", "gswin64c", "gs"):
# No Ghostscript if shutil.which(binary) is not None:
pass gs_windows_binary = binary
return False break
else:
gs_windows_binary = False
gs_binary = gs_windows_binary
else:
try:
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
gs_binary = "gs"
except OSError:
gs_binary = False
return gs_binary is not False
def Ghostscript(tile, size, fp, scale=1, transparency=False): def Ghostscript(tile, size, fp, scale=1, transparency=False):
"""Render an image using Ghostscript""" """Render an image using Ghostscript"""
global gs_binary
if not has_ghostscript():
msg = "Unable to locate Ghostscript on paths"
raise OSError(msg)
# Unpack decoder tile # Unpack decoder tile
decoder, tile, offset, data = tile[0] decoder, tile, offset, data = tile[0]
@ -113,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
# Build Ghostscript command # Build Ghostscript command
command = [ command = [
"gs", gs_binary,
"-q", # quiet mode "-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels) "-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch) "-r%fx%f" % res, # set input DPI (dots per inch)
@ -132,19 +138,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
"showpage", "showpage",
] ]
if gs_windows_binary is not None:
if not gs_windows_binary:
try:
os.unlink(outfile)
if infile_temp:
os.unlink(infile_temp)
except OSError:
pass
msg = "Unable to locate Ghostscript on paths"
raise OSError(msg)
command[0] = gs_windows_binary
# push data through Ghostscript # push data through Ghostscript
try: try:
startupinfo = None startupinfo = None
@ -227,13 +220,15 @@ class EpsImageFile(ImageFile.ImageFile):
# go to offset - start of "%!PS" # go to offset - start of "%!PS"
self.fp.seek(offset) self.fp.seek(offset)
self.mode = "RGB" self._mode = "RGB"
self._size = None self._size = None
byte_arr = bytearray(255) byte_arr = bytearray(255)
bytes_mv = memoryview(byte_arr) bytes_mv = memoryview(byte_arr)
bytes_read = 0 bytes_read = 0
reading_comments = True reading_header_comments = True
reading_trailer_comments = False
trailer_reached = False
def check_required_header_comments(): def check_required_header_comments():
if "PS-Adobe" not in self.info: if "PS-Adobe" not in self.info:
@ -243,6 +238,36 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment' msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
def _read_comment(s):
nonlocal reading_trailer_comments
try:
m = split.match(s)
except re.error as e:
msg = "not an EPS file"
raise SyntaxError(msg) from e
if m:
k, v = m.group(1, 2)
self.info[k] = v
if k == "BoundingBox":
if v == "(atend)":
reading_trailer_comments = True
elif not self._size or (
trailer_reached and reading_trailer_comments
):
try:
# Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers
# put floating point values there anyway.
box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1]
self.tile = [
("eps", (0, 0) + self.size, offset, (length, box))
]
except Exception:
pass
return True
while True: while True:
byte = self.fp.read(1) byte = self.fp.read(1)
if byte == b"": if byte == b"":
@ -265,9 +290,9 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "not an EPS file" msg = "not an EPS file"
raise SyntaxError(msg) raise SyntaxError(msg)
else: else:
if reading_comments: if reading_header_comments:
check_required_header_comments() check_required_header_comments()
reading_comments = False reading_header_comments = False
# reset bytes_read so we can keep reading # reset bytes_read so we can keep reading
# data until the end of the line # data until the end of the line
bytes_read = 0 bytes_read = 0
@ -275,7 +300,7 @@ class EpsImageFile(ImageFile.ImageFile):
bytes_read += 1 bytes_read += 1
continue continue
if reading_comments: if reading_header_comments:
# Load EPS header # Load EPS header
# if this line doesn't start with a "%", # if this line doesn't start with a "%",
@ -283,33 +308,11 @@ class EpsImageFile(ImageFile.ImageFile):
# then we've reached the end of the header/comments # then we've reached the end of the header/comments
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
check_required_header_comments() check_required_header_comments()
reading_comments = False reading_header_comments = False
continue continue
s = str(bytes_mv[:bytes_read], "latin-1") s = str(bytes_mv[:bytes_read], "latin-1")
if not _read_comment(s):
try:
m = split.match(s)
except re.error as e:
msg = "not an EPS file"
raise SyntaxError(msg) from e
if m:
k, v = m.group(1, 2)
self.info[k] = v
if k == "BoundingBox":
try:
# Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers
# put floating point values there anyway.
box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1]
self.tile = [
("eps", (0, 0) + self.size, offset, (length, box))
]
except Exception:
pass
else:
m = field.match(s) m = field.match(s)
if m: if m:
k = m.group(1) k = m.group(1)
@ -339,15 +342,15 @@ class EpsImageFile(ImageFile.ImageFile):
# data start identifier (the image data follows after a single line # data start identifier (the image data follows after a single line
# consisting only of this quoted value) # consisting only of this quoted value)
image_data_values = byte_arr[11:bytes_read].split(None, 7) image_data_values = byte_arr[11:bytes_read].split(None, 7)
columns, rows, bit_depth, mode_id = [ columns, rows, bit_depth, mode_id = (
int(value) for value in image_data_values[:4] int(value) for value in image_data_values[:4]
] )
if bit_depth == 1: if bit_depth == 1:
self.mode = "1" self._mode = "1"
elif bit_depth == 8: elif bit_depth == 8:
try: try:
self.mode = self.mode_map[mode_id] self._mode = self.mode_map[mode_id]
except ValueError: except ValueError:
break break
else: else:
@ -355,7 +358,18 @@ class EpsImageFile(ImageFile.ImageFile):
self._size = columns, rows self._size = columns, rows
return return
elif trailer_reached and reading_trailer_comments:
# Load EPS trailer
# if this line starts with "%%EOF",
# then we've reached the end of the file
if bytes_mv[:5] == b"%%EOF":
break
s = str(bytes_mv[:bytes_read], "latin-1")
_read_comment(s)
elif bytes_mv[:9] == b"%%Trailer":
trailer_reached = True
bytes_read = 0 bytes_read = 0
check_required_header_comments() check_required_header_comments()
@ -391,7 +405,7 @@ class EpsImageFile(ImageFile.ImageFile):
# Load EPS via Ghostscript # Load EPS via Ghostscript
if self.tile: if self.tile:
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
self.mode = self.im.mode self._mode = self.im.mode
self._size = self.im.size self._size = self.im.size
self.tile = [] self.tile = []
return Image.Image.load(self) return Image.Image.load(self)

View File

@ -51,14 +51,14 @@ class FitsImageFile(ImageFile.ImageFile):
number_of_bits = int(headers[b"BITPIX"]) number_of_bits = int(headers[b"BITPIX"])
if number_of_bits == 8: if number_of_bits == 8:
self.mode = "L" self._mode = "L"
elif number_of_bits == 16: elif number_of_bits == 16:
self.mode = "I" self._mode = "I"
# rawmode = "I;16S" # rawmode = "I;16S"
elif number_of_bits == 32: elif number_of_bits == 32:
self.mode = "I" self._mode = "I"
elif number_of_bits in (-32, -64): elif number_of_bits in (-32, -64):
self.mode = "F" self._mode = "F"
# rawmode = "F" if number_of_bits == -32 else "F;64F" # rawmode = "F" if number_of_bits == -32 else "F;64F"
offset = math.ceil(self.fp.tell() / 2880) * 2880 offset = math.ceil(self.fp.tell() / 2880) * 2880

View File

@ -56,7 +56,7 @@ class FliImageFile(ImageFile.ImageFile):
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
# image characteristics # image characteristics
self.mode = "P" self._mode = "P"
self._size = i16(s, 8), i16(s, 10) self._size = i16(s, 8), i16(s, 10)
# animation speed # animation speed

View File

@ -106,7 +106,7 @@ class FpxImageFile(ImageFile.ImageFile):
# note: for now, we ignore the "uncalibrated" flag # note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
self.mode, self.rawmode = MODES[tuple(colors)] self._mode, self.rawmode = MODES[tuple(colors)]
# load JPEG tables, if any # load JPEG tables, if any
self.jpeg = {} self.jpeg = {}

View File

@ -77,7 +77,7 @@ class FtexImageFile(ImageFile.ImageFile):
self._size = struct.unpack("<2i", self.fp.read(8)) self._size = struct.unpack("<2i", self.fp.read(8))
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
self.mode = "RGB" self._mode = "RGB"
# Only support single-format files. # Only support single-format files.
# I don't know of any multi-format file. # I don't know of any multi-format file.
@ -90,7 +90,7 @@ class FtexImageFile(ImageFile.ImageFile):
data = self.fp.read(mipmap_size) data = self.fp.read(mipmap_size)
if format == Format.DXT1: if format == Format.DXT1:
self.mode = "RGBA" self._mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, 1)] self.tile = [("bcn", (0, 0) + self.size, 0, 1)]
elif format == Format.UNCOMPRESSED: elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]

View File

@ -73,9 +73,9 @@ class GbrImageFile(ImageFile.ImageFile):
comment = self.fp.read(comment_length)[:-1] comment = self.fp.read(comment_length)[:-1]
if color_depth == 1: if color_depth == 1:
self.mode = "L" self._mode = "L"
else: else:
self.mode = "RGBA" self._mode = "RGBA"
self._size = width, height self._size = width, height

View File

@ -51,7 +51,7 @@ class GdImageFile(ImageFile.ImageFile):
msg = "Not a valid GD 2.x .gd file" msg = "Not a valid GD 2.x .gd file"
raise SyntaxError(msg) raise SyntaxError(msg)
self.mode = "L" # FIXME: "P" self._mode = "L" # FIXME: "P"
self._size = i16(s, 2), i16(s, 4) self._size = i16(s, 2), i16(s, 4)
true_color = s[6] true_color = s[6]

View File

@ -304,11 +304,11 @@ class GifImageFile(ImageFile.ImageFile):
if frame == 0: if frame == 0:
if self._frame_palette: if self._frame_palette:
if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
self.mode = "RGBA" if frame_transparency is not None else "RGB" self._mode = "RGBA" if frame_transparency is not None else "RGB"
else: else:
self.mode = "P" self._mode = "P"
else: else:
self.mode = "L" self._mode = "L"
if not palette and self.global_palette: if not palette and self.global_palette:
from copy import copy from copy import copy
@ -325,10 +325,10 @@ class GifImageFile(ImageFile.ImageFile):
if "transparency" in self.info: if "transparency" in self.info:
self.im.putpalettealpha(self.info["transparency"], 0) self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
self.mode = "RGBA" self._mode = "RGBA"
del self.info["transparency"] del self.info["transparency"]
else: else:
self.mode = "RGB" self._mode = "RGB"
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
def _rgb(color): def _rgb(color):
@ -424,7 +424,7 @@ class GifImageFile(ImageFile.ImageFile):
self.im.putpalette(*self._frame_palette.getdata()) self.im.putpalette(*self._frame_palette.getdata())
else: else:
self.im = None self.im = None
self.mode = temp_mode self._mode = temp_mode
self._frame_palette = None self._frame_palette = None
super().load_prepare() super().load_prepare()
@ -434,9 +434,9 @@ class GifImageFile(ImageFile.ImageFile):
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
if self._frame_transparency is not None: if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0) self.im.putpalettealpha(self._frame_transparency, 0)
self.mode = "RGBA" self._mode = "RGBA"
else: else:
self.mode = "RGB" self._mode = "RGB"
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
return return
if not self._prev_im: if not self._prev_im:
@ -449,7 +449,7 @@ class GifImageFile(ImageFile.ImageFile):
frame_im = self._crop(frame_im, self.dispose_extent) frame_im = self._crop(frame_im, self.dispose_extent)
self.im = self._prev_im self.im = self._prev_im
self.mode = self.im.mode self._mode = self.im.mode
if frame_im.mode == "RGBA": if frame_im.mode == "RGBA":
self.im.paste(frame_im, self.dispose_extent, frame_im) self.im.paste(frame_im, self.dispose_extent, frame_im)
else: else:
@ -683,11 +683,7 @@ def get_interlace(im):
def _write_local_header(fp, im, offset, flags): def _write_local_header(fp, im, offset, flags):
transparent_color_exists = False transparent_color_exists = False
try: try:
if "transparency" in im.encoderinfo: transparency = int(im.encoderinfo["transparency"])
transparency = im.encoderinfo["transparency"]
else:
transparency = im.info["transparency"]
transparency = int(transparency)
except (KeyError, ValueError): except (KeyError, ValueError):
pass pass
else: else:
@ -916,7 +912,7 @@ def _get_global_header(im, info):
info info
and ( and (
"transparency" in info "transparency" in info
or "loop" in info or info.get("loop") is not None
or info.get("duration") or info.get("duration")
or info.get("comment") or info.get("comment")
) )
@ -941,7 +937,7 @@ def _get_global_header(im, info):
# Global Color Table # Global Color Table
_get_header_palette(palette_bytes), _get_header_palette(palette_bytes),
] ]
if "loop" in info: if info.get("loop") is not None:
header.append( header.append(
b"!" b"!"
+ o8(255) # extension intro + o8(255) # extension intro

View File

@ -46,7 +46,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
self.fp.seek(offset) self.fp.seek(offset)
# make something up # make something up
self.mode = "F" self._mode = "F"
self._size = 1, 1 self._size = 1, 1
loader = self._load() loader = self._load()

View File

@ -46,7 +46,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
self.fp.seek(offset) self.fp.seek(offset)
# make something up # make something up
self.mode = "F" self._mode = "F"
self._size = 1, 1 self._size = 1, 1
loader = self._load() loader = self._load()

View File

@ -253,7 +253,7 @@ class IcnsImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
self.icns = IcnsFile(self.fp) self.icns = IcnsFile(self.fp)
self.mode = "RGBA" self._mode = "RGBA"
self.info["sizes"] = self.icns.itersizes() self.info["sizes"] = self.icns.itersizes()
self.best_size = self.icns.bestsize() self.best_size = self.icns.bestsize()
self.size = ( self.size = (
@ -305,7 +305,7 @@ class IcnsImageFile(ImageFile.ImageFile):
px = im.load() px = im.load()
self.im = im.im self.im = im.im
self.mode = im.mode self._mode = im.mode
self.size = im.size self.size = im.size
return px return px

View File

@ -330,7 +330,7 @@ class IcoImageFile(ImageFile.ImageFile):
im.load() im.load()
self.im = im.im self.im = im.im
self.pyaccess = None self.pyaccess = None
self.mode = im.mode self._mode = im.mode
if im.size != self.size: if im.size != self.size:
warnings.warn("Image was not the expected size") warnings.warn("Image was not the expected size")

View File

@ -205,7 +205,7 @@ class ImImageFile(ImageFile.ImageFile):
# Basic attributes # Basic attributes
self._size = self.info[SIZE] self._size = self.info[SIZE]
self.mode = self.info[MODE] self._mode = self.info[MODE]
# Skip forward to start of image data # Skip forward to start of image data
while s and s[:1] != b"\x1A": while s and s[:1] != b"\x1A":
@ -231,9 +231,9 @@ class ImImageFile(ImageFile.ImageFile):
self.lut = list(palette[:256]) self.lut = list(palette[:256])
else: else:
if self.mode in ["L", "P"]: if self.mode in ["L", "P"]:
self.mode = self.rawmode = "P" self._mode = self.rawmode = "P"
elif self.mode in ["LA", "PA"]: elif self.mode in ["LA", "PA"]:
self.mode = "PA" self._mode = "PA"
self.rawmode = "PA;L" self.rawmode = "PA;L"
self.palette = ImagePalette.raw("RGB;L", palette) self.palette = ImagePalette.raw("RGB;L", palette)
elif self.mode == "RGB": elif self.mode == "RGB":

View File

@ -298,7 +298,11 @@ _initialized = 0
def preinit(): def preinit():
"""Explicitly load standard file format drivers.""" """
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.
It is called when opening or saving images.
"""
global _initialized global _initialized
if _initialized >= 1: if _initialized >= 1:
@ -334,11 +338,6 @@ def preinit():
assert PngImagePlugin assert PngImagePlugin
except ImportError: except ImportError:
pass pass
# try:
# import TiffImagePlugin
# assert TiffImagePlugin
# except ImportError:
# pass
_initialized = 1 _initialized = 1
@ -347,6 +346,9 @@ def init():
""" """
Explicitly initializes the Python Imaging Library. This function Explicitly initializes the Python Imaging Library. This function
loads all available file format drivers. loads all available file format drivers.
It is called when opening or saving images if :py:meth:`~preinit()` is
insufficient, and by :py:meth:`~PIL.features.pilinfo`.
""" """
global _initialized global _initialized
@ -482,7 +484,7 @@ class Image:
# FIXME: take "new" parameters / other image? # FIXME: take "new" parameters / other image?
# FIXME: turn mode and size into delegating properties? # FIXME: turn mode and size into delegating properties?
self.im = None self.im = None
self.mode = "" self._mode = ""
self._size = (0, 0) self._size = (0, 0)
self.palette = None self.palette = None
self.info = {} self.info = {}
@ -502,10 +504,14 @@ class Image:
def size(self): def size(self):
return self._size return self._size
@property
def mode(self):
return self._mode
def _new(self, im): def _new(self, im):
new = Image() new = Image()
new.im = im new.im = im
new.mode = im.mode new._mode = im.mode
new._size = im.size new._size = im.size
if im.mode in ("P", "PA"): if im.mode in ("P", "PA"):
if self.palette: if self.palette:
@ -641,9 +647,8 @@ class Image:
b = io.BytesIO() b = io.BytesIO()
try: try:
self.save(b, image_format, **kwargs) self.save(b, image_format, **kwargs)
except Exception as e: except Exception:
msg = f"Could not save to {image_format} for display" return None
raise ValueError(msg) from e
return b.getvalue() return b.getvalue()
def _repr_png_(self): def _repr_png_(self):
@ -693,7 +698,7 @@ class Image:
Image.__init__(self) Image.__init__(self)
info, mode, size, palette, data = state info, mode, size, palette, data = state
self.info = info self.info = info
self.mode = mode self._mode = mode
self._size = size self._size = size
self.im = core.new(mode, size) self.im = core.new(mode, size)
if mode in ("L", "LA", "P", "PA") and palette: if mode in ("L", "LA", "P", "PA") and palette:
@ -910,7 +915,7 @@ class Image:
self.load() self.load()
has_transparency = self.info.get("transparency") is not None has_transparency = "transparency" in self.info
if not mode and self.mode == "P": if not mode and self.mode == "P":
# determine default mode # determine default mode
if self.palette: if self.palette:
@ -1069,7 +1074,7 @@ class Image:
if mode == "P" and palette != Palette.ADAPTIVE: if mode == "P" and palette != Palette.ADAPTIVE:
from . import ImagePalette from . import ImagePalette
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
if delete_trns: if delete_trns:
# crash fail if we leave a bytes transparency in an rgb/l mode. # crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"] del new_im.info["transparency"]
@ -1526,6 +1531,24 @@ class Image:
rawmode = mode rawmode = mode
return list(self.im.getpalette(mode, rawmode)) return list(self.im.getpalette(mode, rawmode))
@property
def has_transparency_data(self) -> bool:
"""
Determine if an image has transparency data, whether in the form of an
alpha channel, a palette with an alpha channel, or a "transparency" key
in the info dictionary.
Note the image might still appear solid, if all of the values shown
within are opaque.
:returns: A boolean.
"""
return (
self.mode in ("LA", "La", "PA", "RGBA", "RGBa")
or (self.mode == "P" and self.palette.mode.endswith("A"))
or "transparency" in self.info
)
def apply_transparency(self): def apply_transparency(self):
""" """
If a P mode image has a "transparency" key in the info dictionary, If a P mode image has a "transparency" key in the info dictionary,
@ -1562,7 +1585,7 @@ class Image:
self.load() self.load()
if self.pyaccess: if self.pyaccess:
return self.pyaccess.getpixel(xy) return self.pyaccess.getpixel(xy)
return self.im.getpixel(xy) return self.im.getpixel(tuple(xy))
def getprojection(self): def getprojection(self):
""" """
@ -1840,7 +1863,7 @@ class Image:
raise ValueError from e # sanity check raise ValueError from e # sanity check
self.im = im self.im = im
self.pyaccess = None self.pyaccess = None
self.mode = self.im.mode self._mode = self.im.mode
except KeyError as e: except KeyError as e:
msg = "illegal image mode" msg = "illegal image mode"
raise ValueError(msg) from e raise ValueError(msg) from e
@ -1918,7 +1941,7 @@ class Image:
if not isinstance(data, bytes): if not isinstance(data, bytes):
data = bytes(data) data = bytes(data)
palette = ImagePalette.raw(rawmode, data) palette = ImagePalette.raw(rawmode, data)
self.mode = "PA" if "A" in self.mode else "P" self._mode = "PA" if "A" in self.mode else "P"
self.palette = palette self.palette = palette
self.palette.mode = "RGB" self.palette.mode = "RGB"
self.load() # install new palette self.load() # install new palette
@ -2026,7 +2049,7 @@ class Image:
mapping_palette = bytearray(new_positions) mapping_palette = bytearray(new_positions)
m_im = self.copy() m_im = self.copy()
m_im.mode = "P" m_im._mode = "P"
m_im.palette = ImagePalette.ImagePalette( m_im.palette = ImagePalette.ImagePalette(
palette_mode, palette=mapping_palette * bands palette_mode, palette=mapping_palette * bands
@ -2601,7 +2624,7 @@ class Image:
self.im = im.im self.im = im.im
self._size = size self._size = size
self.mode = self.im.mode self._mode = self.im.mode
self.readonly = 0 self.readonly = 0
self.pyaccess = None self.pyaccess = None
@ -2997,7 +3020,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
if args == (): if args == ():
args = mode, 0, 1 args = mode, 0, 1
if args[0] in _MAPMODES: if args[0] in _MAPMODES:
im = new(mode, (1, 1)) im = new(mode, (0, 0))
im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) im = im._new(core.map_buffer(data, size, decoder_name, 0, args))
if mode == "P": if mode == "P":
from . import ImagePalette from . import ImagePalette
@ -3404,8 +3427,12 @@ def register_open(id, factory, accept=None):
def register_mime(id, mimetype): def register_mime(id, mimetype):
""" """
Registers an image MIME type. This function should not be used Registers an image MIME type by populating ``Image.MIME``. This function
in application code. should not be used in application code.
``Image.MIME`` provides a mapping from image format identifiers to mime
formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can
provide a different result for specific images.
:param id: An image format identifier. :param id: An image format identifier.
:param mimetype: The image MIME type for this format. :param mimetype: The image MIME type for this format.

View File

@ -157,7 +157,8 @@ class GaussianBlur(MultibandFilter):
approximates a Gaussian kernel. For details on accuracy see approximates a Gaussian kernel. For details on accuracy see
<https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf> <https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf>
:param radius: Standard deviation of the Gaussian kernel. :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two
numbers for x and y, or a single number for both.
""" """
name = "GaussianBlur" name = "GaussianBlur"
@ -166,7 +167,12 @@ class GaussianBlur(MultibandFilter):
self.radius = radius self.radius = radius
def filter(self, image): def filter(self, image):
return image.gaussian_blur(self.radius) xy = self.radius
if not isinstance(xy, (tuple, list)):
xy = (xy, xy)
if xy == (0, 0):
return image.copy()
return image.gaussian_blur(xy)
class BoxBlur(MultibandFilter): class BoxBlur(MultibandFilter):
@ -176,21 +182,31 @@ class BoxBlur(MultibandFilter):
which runs in linear time relative to the size of the image which runs in linear time relative to the size of the image
for any radius value. for any radius value.
:param radius: Size of the box in one direction. Radius 0 does not blur, :param radius: Size of the box in a direction. Either a sequence of two numbers for
returns an identical image. Radius 1 takes 1 pixel x and y, or a single number for both.
in each direction, i.e. 9 pixels in total.
Radius 0 does not blur, returns an identical image.
Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total.
""" """
name = "BoxBlur" name = "BoxBlur"
def __init__(self, radius): def __init__(self, radius):
if radius < 0: xy = radius
if not isinstance(xy, (tuple, list)):
xy = (xy, xy)
if xy[0] < 0 or xy[1] < 0:
msg = "radius must be >= 0" msg = "radius must be >= 0"
raise ValueError(msg) raise ValueError(msg)
self.radius = radius self.radius = radius
def filter(self, image): def filter(self, image):
return image.box_blur(self.radius) xy = self.radius
if not isinstance(xy, (tuple, list)):
xy = (xy, xy)
if xy == (0, 0):
return image.copy()
return image.box_blur(xy)
class UnsharpMask(MultibandFilter): class UnsharpMask(MultibandFilter):

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