mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 09:26:16 +03:00
Merge branch 'main' into jpeg_xmp
This commit is contained in:
commit
f24222a954
|
@ -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\
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -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
|
||||||
|
|
4
.github/workflows/macos-install.sh
vendored
4
.github/workflows/macos-install.sh
vendored
|
@ -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
|
||||||
|
|
32
.github/workflows/test-cygwin.yml
vendored
32
.github/workflows/test-cygwin.yml
vendored
|
@ -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}"
|
||||||
|
|
10
.github/workflows/test-docker.yml
vendored
10
.github/workflows/test-docker.yml
vendored
|
@ -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
|
||||||
|
|
10
.github/workflows/test-mingw.yml
vendored
10
.github/workflows/test-mingw.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
2
.github/workflows/test-valgrind.yml
vendored
|
@ -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
|
||||||
|
|
16
.github/workflows/test-windows.yml
vendored
16
.github/workflows/test-windows.yml
vendored
|
@ -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
|
||||||
|
|
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
|
@ -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
40
.github/workflows/wheels-build.sh
vendored
Executable 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
69
.github/workflows/wheels-linux.yml
vendored
Normal 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
57
.github/workflows/wheels-macos.yml
vendored
Normal 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
42
.github/workflows/wheels.yml
vendored
Normal 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
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "multibuild"]
|
||||||
|
path = wheels/multibuild
|
||||||
|
url = https://github.com/multi-build/multibuild.git
|
|
@ -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
135
.travis.yml
Normal 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
|
112
CHANGES.rst
112
CHANGES.rst
|
@ -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).
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
10
README.md
10
README.md
|
@ -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>
|
||||||
|
|
17
RELEASING.md
17
RELEASING.md
|
@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
* [ ] 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
0
Tests/check_j2k_leaks.py
Executable file → Normal 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
|
||||||
|
|
|
@ -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
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
0
Tests/images/negative_size.ppm
Executable file → Normal file
BIN
Tests/images/palette.dds
Normal file
BIN
Tests/images/palette.dds
Normal file
Binary file not shown.
BIN
Tests/images/zero_bb_eof_before_boundingbox.eps
Normal file
BIN
Tests/images/zero_bb_eof_before_boundingbox.eps
Normal file
Binary file not shown.
BIN
Tests/images/zero_bb_trailer.eps
Normal file
BIN
Tests/images/zero_bb_trailer.eps
Normal file
Binary file not shown.
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
j2k.load()
|
im.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"))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
0
_custom_build/backend.py
Executable file → Normal 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,3 @@ make && sudo make install
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,3 @@
|
||||||
|
|
||||||
pkg install -y python ndk-sysroot clang make \
|
pkg install -y python ndk-sysroot clang make \
|
||||||
libjpeg-turbo
|
libjpeg-turbo
|
||||||
|
|
||||||
|
|
|
@ -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/
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 |
|
||||||
| +---------------------------+------------------+--------------+
|
| +---------------------------+------------------+--------------+
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
|
@ -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
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
14
docs/releasenotes/10.0.1.rst
Normal file
14
docs/releasenotes/10.0.1.rst
Normal 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.
|
66
docs/releasenotes/10.1.0.rst
Normal file
66
docs/releasenotes/10.1.0.rst
Normal 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.
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -37,8 +37,15 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
def has_ghostscript():
|
||||||
|
global gs_binary, gs_windows_binary
|
||||||
|
if gs_binary is None:
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
|
if gs_windows_binary is None:
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
for binary in ("gswin32c", "gswin64c", "gs"):
|
for binary in ("gswin32c", "gswin64c", "gs"):
|
||||||
|
@ -47,23 +54,22 @@ if sys.platform.startswith("win"):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
gs_windows_binary = False
|
gs_windows_binary = False
|
||||||
|
gs_binary = gs_windows_binary
|
||||||
|
else:
|
||||||
def has_ghostscript():
|
|
||||||
if gs_windows_binary:
|
|
||||||
return True
|
|
||||||
if not sys.platform.startswith("win"):
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
||||||
return True
|
gs_binary = "gs"
|
||||||
except OSError:
|
except OSError:
|
||||||
# No Ghostscript
|
gs_binary = False
|
||||||
pass
|
return gs_binary is not False
|
||||||
return 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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -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))]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user