mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-24 17:06:16 +03:00
Merge branch 'main' into jpeg_xmp
This commit is contained in:
commit
f24222a954
|
@ -21,13 +21,11 @@ environment:
|
|||
install:
|
||||
- '%PYTHON%\%EXECUTABLE% --version'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
|
||||
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
||||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||
- 7z x pillow-depends.zip -oc:\
|
||||
- 7z x pillow-test-images.zip -oc:\
|
||||
- mv c:\pillow-depends-main c:\pillow-depends
|
||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||
- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.0.0.20230317
|
||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
|
|
|
@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
|||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard
|
||||
sway wl-clipboard libopenblas-dev
|
||||
fi
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
|
@ -38,11 +38,10 @@ python3 -m pip install -U pytest-timeout
|
|||
python3 -m pip install pyroma
|
||||
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
# TODO Remove condition when NumPy supports 3.12
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# PyQt6 doesn't support PyPy3
|
||||
if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
||||
python3 -m pip install pyqt6
|
||||
fi
|
||||
|
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
|||
name: Docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
name: Lint
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: pre-commit cache
|
||||
uses: actions/cache@v3
|
||||
|
|
4
.github/workflows/macos-install.sh
vendored
4
.github/workflows/macos-install.sh
vendored
|
@ -3,6 +3,7 @@
|
|||
set -e
|
||||
|
||||
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
|
||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
||||
python3 -m pip install coverage
|
||||
|
@ -13,8 +14,7 @@ python3 -m pip install -U pytest-cov
|
|||
python3 -m pip install -U pytest-timeout
|
||||
python3 -m pip install pyroma
|
||||
|
||||
# TODO Remove condition when NumPy supports 3.12
|
||||
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
|
||||
python3 -m pip install numpy
|
||||
|
||||
# extra test images
|
||||
pushd depends && ./install_extra_test_images.sh && popd
|
||||
|
|
32
.github/workflows/test-cygwin.yml
vendored
32
.github/workflows/test-cygwin.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
|||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -36,7 +44,7 @@ jobs:
|
|||
git config --global core.autocrlf input
|
||||
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: cygwin/cygwin-install-action@v4
|
||||
|
@ -76,17 +84,23 @@ jobs:
|
|||
with:
|
||||
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
||||
|
||||
- name: Select Python version
|
||||
run: |
|
||||
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
||||
|
||||
- name: Get latest NumPy version
|
||||
id: latest-numpy
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
run: |
|
||||
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: 'C:\cygwin\home\runneradmin\.cache\pip'
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
|
||||
|
||||
- name: Select Python version
|
||||
run: |
|
||||
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3
|
||||
${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-
|
||||
|
||||
- name: Build system information
|
||||
run: |
|
||||
|
@ -96,10 +110,10 @@ jobs:
|
|||
run: |
|
||||
bash.exe .ci/install.sh
|
||||
|
||||
- name: Install a different NumPy
|
||||
- name: Upgrade NumPy
|
||||
shell: dash.exe -l "{0}"
|
||||
run: |
|
||||
python3 -m pip install -U numpy
|
||||
python3 -m pip install -U "numpy<1.26"
|
||||
|
||||
- name: Build
|
||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||
|
|
10
.github/workflows/test-docker.yml
vendored
10
.github/workflows/test-docker.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
|||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -59,7 +67,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
10
.github/workflows/test-mingw.yml
vendored
10
.github/workflows/test-mingw.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
|||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -34,7 +42,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up shell
|
||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
2
.github/workflows/test-valgrind.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
name: ${{ matrix.docker }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build system information
|
||||
run: python3 .github/workflows/system-info.py
|
||||
|
|
16
.github/workflows/test-windows.yml
vendored
16
.github/workflows/test-windows.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
|||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -24,7 +32,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
|
||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
|
@ -32,16 +40,16 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout cached dependencies
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: python-pillow/pillow-depends
|
||||
path: winbuild\depends
|
||||
|
||||
- name: Checkout extra test images
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: python-pillow/test-images
|
||||
path: Tests\test-images
|
||||
|
|
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
|||
push:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- ".github/workflows/docs.yml"
|
||||
- ".github/workflows/wheels*"
|
||||
- ".gitmodules"
|
||||
- ".travis.yml"
|
||||
- "docs/**"
|
||||
- "wheels/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
@ -31,7 +39,7 @@ jobs:
|
|||
python-version: [
|
||||
"pypy3.10",
|
||||
"pypy3.9",
|
||||
"3.12-dev",
|
||||
"3.12",
|
||||
"3.11",
|
||||
"3.10",
|
||||
"3.9",
|
||||
|
@ -48,7 +56,7 @@ jobs:
|
|||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
|
|
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:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.13.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.9.1
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--target-version=py38]
|
||||
|
@ -23,17 +29,17 @@ repos:
|
|||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.1
|
||||
rev: v1.5.4
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat]
|
||||
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
|
||||
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
|
@ -44,23 +50,28 @@ repos:
|
|||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-merge-conflict
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^Tests/images/
|
||||
- id: trailing-whitespace
|
||||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v0.6.7
|
||||
rev: v0.6.8
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 0.12.1
|
||||
rev: 1.2.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.13
|
||||
rev: v0.14
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
|
|
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)
|
||||
-------------------
|
||||
|
||||
- Allow saving I;16B images as PNG #7302
|
||||
[radarhere]
|
||||
|
||||
- Corrected drawing I;16 points and writing I;16 text #7257
|
||||
[radarhere]
|
||||
|
||||
- Set blue channel to 128 for BC5S #7413
|
||||
[radarhere]
|
||||
|
||||
- Increase flexibility when reading IPTC fields #7319
|
||||
[radarhere]
|
||||
|
||||
- Set C palette to be empty by default #7289
|
||||
[radarhere]
|
||||
|
||||
- Added gs_binary to control Ghostscript use on all platforms #7392
|
||||
[radarhere]
|
||||
|
||||
- Read bounding box information from the trailer of EPS files if specified #7382
|
||||
[nopperl, radarhere]
|
||||
|
||||
- Added reading 8-bit color DDS images #7426
|
||||
[radarhere]
|
||||
|
||||
- Added has_transparency_data #7420
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Fixed bug when reading BC5S DDS images #7401
|
||||
[radarhere]
|
||||
|
||||
- Prevent TIFF orientation from being applied more than once #7383
|
||||
[radarhere]
|
||||
|
||||
- Use previous pixel alpha for QOI_OP_RGB #7357
|
||||
[radarhere]
|
||||
|
||||
- Added BC5U reading #7358
|
||||
[radarhere]
|
||||
|
||||
- Allow getpixel() to accept a list #7355
|
||||
[radarhere, homm]
|
||||
|
||||
- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336
|
||||
[radarhere]
|
||||
|
||||
- Expand JPEG buffer size when saving optimized or progressive #7345
|
||||
[radarhere]
|
||||
|
||||
- Added session type check for Linux in ImageGrab.grabclipboard() #7332
|
||||
[TheNooB2706, radarhere, hugovk]
|
||||
|
||||
- Allow "loop=None" when saving GIF images #7329
|
||||
[radarhere]
|
||||
|
||||
- Fixed transparency when saving P mode images to PDF #7323
|
||||
[radarhere]
|
||||
|
||||
- Added saving LA images as PDFs #7299
|
||||
[radarhere]
|
||||
|
||||
- Set SMaskInData to 1 for PDFs with alpha #7316, #7317
|
||||
[radarhere]
|
||||
|
||||
- Changed Image mode property to be read-only by default #7307
|
||||
[radarhere]
|
||||
|
||||
- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266
|
||||
[mtreinish, radarhere]
|
||||
|
||||
- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284
|
||||
[radarhere]
|
||||
|
||||
- Fix missing symbols when libtiff depends on libjpeg #7270
|
||||
[heitbaum]
|
||||
|
||||
10.0.1 (2023-09-15)
|
||||
-------------------
|
||||
|
||||
- Updated libwebp to 1.3.2 #7395
|
||||
[radarhere]
|
||||
|
||||
- Updated zlib to 1.3 #7344
|
||||
[radarhere]
|
||||
|
||||
10.0.0 (2023-07-01)
|
||||
-------------------
|
||||
|
||||
|
@ -5735,8 +5816,8 @@ http://svn.effbot.org/public/pil/
|
|||
a polyline, independent of line angle.
|
||||
|
||||
- Fixed bearing calculation and clipping in the ImageFont truetype
|
||||
renderer; this could lead to clipped text, or crashes in the low-
|
||||
level _imagingft module. (based on input from Adam Twardoch and
|
||||
renderer; this could lead to clipped text, or crashes in the low-level
|
||||
_imagingft module. (based on input from Adam Twardoch and
|
||||
others).
|
||||
|
||||
- Added ImageQt wrapper module, for converting PIL Image objects to
|
||||
|
@ -5817,8 +5898,7 @@ http://svn.effbot.org/public/pil/
|
|||
1.1.5c2 and 1.1.5 final
|
||||
-----------------------
|
||||
|
||||
- Added experimental PERSPECTIVE transform method (from Jeff Breiden-
|
||||
bach).
|
||||
- Added experimental PERSPECTIVE transform method (from Jeff Breidenbach).
|
||||
|
||||
1.1.5c1
|
||||
-------
|
||||
|
@ -5890,8 +5970,8 @@ http://svn.effbot.org/public/pil/
|
|||
|
||||
- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA".
|
||||
|
||||
- Added "getcolors()" method. This is similar to the existing histo-
|
||||
gram method, but looks at color values instead of individual layers,
|
||||
- Added "getcolors()" method. This is similar to the existing histogram
|
||||
method, but looks at color values instead of individual layers,
|
||||
and returns an unsorted list of (count, color) tuples.
|
||||
|
||||
By default, the method returns None if finds more than 256 colors.
|
||||
|
@ -6107,8 +6187,8 @@ http://svn.effbot.org/public/pil/
|
|||
|
||||
- Added limited support for "bitfield compression" in BMP files
|
||||
and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This
|
||||
also fixes a problem with ImageGrab module when copying screen-
|
||||
dumps from the clipboard on 15/16/32-bit displays.
|
||||
also fixes a problem with ImageGrab module when copying screendumps
|
||||
from the clipboard on 15/16/32-bit displays.
|
||||
|
||||
- Added experimental WAL (Quake 2 textures) loader. To use this
|
||||
loader, import WalImageFile and call the "open" method in that
|
||||
|
@ -6219,8 +6299,8 @@ http://svn.effbot.org/public/pil/
|
|||
1.1.3 final
|
||||
-----------
|
||||
|
||||
- Made setup.py look for old versions of zlib. For some back-
|
||||
ground, see: https://zlib.net/advisory-2002-03-11.txt
|
||||
- Made setup.py look for old versions of zlib. For some background,
|
||||
see: https://zlib.net/advisory-2002-03-11.txt
|
||||
|
||||
1.1.3c2
|
||||
-------
|
||||
|
@ -6411,8 +6491,8 @@ http://svn.effbot.org/public/pil/
|
|||
supports all major PIL image modes (including F and I).
|
||||
|
||||
- The ImageFile module now includes a Parser class, which can
|
||||
be used to incrementally decode an image file (while down-
|
||||
loading it from the net, for example). See the handbook for
|
||||
be used to incrementally decode an image file (while downloading
|
||||
it from the net, for example). See the handbook for
|
||||
details.
|
||||
|
||||
- "show" now converts non-standard modes to "L" or "RGB" (as
|
||||
|
@ -6550,8 +6630,8 @@ http://svn.effbot.org/public/pil/
|
|||
|
||||
- The Image "transform" method now supports Image.QUAD transforms.
|
||||
The data argument is an 8-tuple giving the upper left, lower
|
||||
left, lower right, and upper right corner of the source quadri-
|
||||
lateral. Also added Image.MESH transform which takes a list
|
||||
left, lower right, and upper right corner of the source quadrilateral.
|
||||
Also added Image.MESH transform which takes a list
|
||||
of quadrilaterals.
|
||||
|
||||
- The Image "resize", "rotate", and "transform" methods now support
|
||||
|
@ -6776,8 +6856,8 @@ The test suite includes 400 individual tests.
|
|||
neither "short", "int" nor "long" are 32-bit wide.
|
||||
|
||||
- Added file= and data= keyword arguments to PhotoImage and BitmapImage.
|
||||
This allows you to use them as drop-in replacements for the corre-
|
||||
sponding Tkinter classes.
|
||||
This allows you to use them as drop-in replacements for the corresponding
|
||||
Tkinter classes.
|
||||
|
||||
- Removed bogus references to the crack coder (ImagingCrack).
|
||||
|
||||
|
|
|
@ -29,3 +29,4 @@ global-exclude .git*
|
|||
global-exclude *.pyc
|
||||
global-exclude *.so
|
||||
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
|
||||
alt="AppVeyor CI build status (Windows)"
|
||||
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
|
||||
alt="GitHub Actions wheels build status (Wheels)"
|
||||
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a>
|
||||
<a href="https://app.travis-ci.com/github/python-pillow/pillow-wheels"><img
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
|
||||
alt="GitHub Actions build status (Wheels)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
|
||||
<a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
|
||||
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
|
||||
alt="Code coverage"
|
||||
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
|
||||
* [ ] Develop and prepare release in `main` branch.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
||||
* [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions.
|
||||
* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them.
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Update `CHANGES.rst`.
|
||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||
|
@ -99,17 +99,14 @@ Released as needed privately to individual vendors for critical security-related
|
|||
## Binary Distributions
|
||||
|
||||
### macOS and Linux
|
||||
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
||||
* [ ] Download wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
|
||||
```bash
|
||||
git clone https://github.com/python-pillow/pillow-wheels
|
||||
cd pillow-wheels
|
||||
./update-pillow-tag.sh [[release tag]]
|
||||
```
|
||||
* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
|
||||
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo:
|
||||
```bash
|
||||
gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels
|
||||
gh run download --dir dist
|
||||
# select dist-x.y.z
|
||||
```
|
||||
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
|
||||
and copy into `dist`.
|
||||
|
||||
### Windows
|
||||
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
||||
|
|
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:
|
||||
try:
|
||||
url = test_image_results.upload(a, b)
|
||||
logger.error(f"Url for test images: {url}")
|
||||
logger.error("URL for test images: %s", url)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
@ -126,7 +126,7 @@ def assert_image_similar(a, b, epsilon, msg=None):
|
|||
if HAS_UPLOADER:
|
||||
try:
|
||||
url = test_image_results.upload(a, b)
|
||||
logger.error(f"Url for test images: {url}")
|
||||
logger.exception("URL for test images: %s", url)
|
||||
except Exception:
|
||||
pass
|
||||
raise e
|
||||
|
|
|
@ -22,4 +22,3 @@ and that the name of ICC shall not be used in advertising or publicity
|
|||
pertaining to distribution of the software without specific, written
|
||||
prior permission. ICC makes no representations about the suitability
|
||||
of this software for any purpose.
|
||||
|
||||
|
|
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
|
||||
|
||||
from PIL import Image, features
|
||||
from Tests.helper import skip_unless_feature
|
||||
|
||||
if sys.platform.startswith("win32"):
|
||||
pytest.skip("Fuzzer is linux only", allow_module_level=True)
|
||||
|
@ -48,6 +49,7 @@ def test_fuzz_images(path):
|
|||
fuzzers.disable_decompressionbomb_error()
|
||||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
@pytest.mark.parametrize(
|
||||
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
|
||||
)
|
||||
|
|
|
@ -22,7 +22,7 @@ def test_imageops_box_blur():
|
|||
|
||||
|
||||
def box_blur(image, radius=1, n=1):
|
||||
return image._new(image.im.box_blur(radius, n))
|
||||
return image._new(image.im.box_blur((radius, radius), n))
|
||||
|
||||
|
||||
def assert_image(im, data, delta=0):
|
||||
|
|
|
@ -16,6 +16,7 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
|
|||
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
||||
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
||||
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
|
||||
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
|
||||
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
|
||||
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
|
||||
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
|
||||
|
@ -81,10 +82,18 @@ def test_sanity_ati1():
|
|||
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
|
||||
|
||||
|
||||
def test_sanity_ati2():
|
||||
"""Check ATI2 images can be opened"""
|
||||
@pytest.mark.parametrize(
|
||||
"image_path",
|
||||
(
|
||||
TEST_FILE_ATI2,
|
||||
# hexeditted to use BC5U FourCC
|
||||
TEST_FILE_BC5U,
|
||||
),
|
||||
)
|
||||
def test_sanity_ati2_bc5u(image_path):
|
||||
"""Check ATI2 and BC5U images can be opened"""
|
||||
|
||||
with Image.open(TEST_FILE_ATI2) as im:
|
||||
with Image.open(image_path) as im:
|
||||
im.load()
|
||||
|
||||
assert im.format == "DDS"
|
||||
|
@ -289,6 +298,11 @@ def test_dxt5_colorblock_alpha_issue_4142():
|
|||
assert px[2] != 0
|
||||
|
||||
|
||||
def test_palette():
|
||||
with Image.open("Tests/images/palette.dds") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||
|
||||
|
||||
def test_unimplemented_pixel_format():
|
||||
with pytest.raises(NotImplementedError):
|
||||
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
|
||||
|
|
|
@ -8,6 +8,7 @@ from .helper import (
|
|||
assert_image_similar,
|
||||
assert_image_similar_tofile,
|
||||
hopper,
|
||||
is_win32,
|
||||
mark_if_feature_version,
|
||||
skip_unless_feature,
|
||||
)
|
||||
|
@ -98,6 +99,20 @@ def test_load():
|
|||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
|
||||
|
||||
def test_binary():
|
||||
if HAS_GHOSTSCRIPT:
|
||||
assert EpsImagePlugin.gs_binary is not None
|
||||
else:
|
||||
assert EpsImagePlugin.gs_binary is False
|
||||
|
||||
if not is_win32():
|
||||
assert EpsImagePlugin.gs_windows_binary is None
|
||||
elif not HAS_GHOSTSCRIPT:
|
||||
assert EpsImagePlugin.gs_windows_binary is False
|
||||
else:
|
||||
assert EpsImagePlugin.gs_windows_binary is not None
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
with pytest.raises(SyntaxError):
|
||||
|
@ -404,3 +419,18 @@ def test_timeout(test_file):
|
|||
with pytest.raises(Image.UnidentifiedImageError):
|
||||
with Image.open(f):
|
||||
pass
|
||||
|
||||
|
||||
def test_bounding_box_in_trailer():
|
||||
# Check bounding boxes are parsed in the same way
|
||||
# when specified in the header and the trailer
|
||||
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
|
||||
FILE1
|
||||
) as header_image:
|
||||
assert trailer_image.size == header_image.size
|
||||
|
||||
|
||||
def test_eof_before_bounding_box():
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||
pass
|
||||
|
|
|
@ -205,14 +205,14 @@ def test_optimize_full_l():
|
|||
|
||||
|
||||
def test_optimize_if_palette_can_be_reduced_by_half():
|
||||
with Image.open("Tests/images/test.colors.gif") as im:
|
||||
# Reduce dimensions because original is too big for _get_optimize()
|
||||
im = im.resize((591, 443))
|
||||
im_rgb = im.convert("RGB")
|
||||
im = Image.new("P", (8, 1))
|
||||
im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
|
||||
for i in range(8):
|
||||
im.putpixel((i, 0), (i + 1, 0, 0))
|
||||
|
||||
for optimize, colors in ((False, 256), (True, 8)):
|
||||
out = BytesIO()
|
||||
im_rgb.save(out, "GIF", optimize=optimize)
|
||||
im.save(out, "GIF", optimize=optimize)
|
||||
with Image.open(out) as reloaded:
|
||||
assert len(reloaded.palette.palette) // 3 == colors
|
||||
|
||||
|
@ -875,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
|
|||
assert reread.info["duration"] == 8500
|
||||
|
||||
|
||||
def test_loop_none(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
im.save(out, loop=None)
|
||||
with Image.open(out) as reread:
|
||||
assert "loop" not in reread.info
|
||||
|
||||
|
||||
def test_number_of_loops(tmp_path):
|
||||
number_of_loops = 2
|
||||
|
||||
|
@ -1086,6 +1094,21 @@ def test_transparent_optimize(tmp_path):
|
|||
assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
|
||||
|
||||
|
||||
def test_removed_transparency(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im = Image.new("RGB", (256, 1))
|
||||
|
||||
for x in range(256):
|
||||
im.putpixel((x, 0), (x, 0, 0))
|
||||
|
||||
im.info["transparency"] = (255, 255, 255)
|
||||
with pytest.warns(UserWarning):
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert "transparency" not in reloaded.info
|
||||
|
||||
|
||||
def test_rgb_transparency(tmp_path):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
|
||||
|
@ -1157,18 +1180,17 @@ def test_palette_save_L(tmp_path):
|
|||
|
||||
|
||||
def test_palette_save_P(tmp_path):
|
||||
# Pass in a different palette, then construct what the image would look like.
|
||||
# Forcing a non-straight grayscale palette.
|
||||
|
||||
im = hopper("P")
|
||||
palette = bytes(255 - i // 3 for i in range(768))
|
||||
im = Image.new("P", (1, 2))
|
||||
im.putpixel((0, 1), 1)
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im.save(out, palette=palette)
|
||||
im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
im.putpalette(palette)
|
||||
assert_image_equal(reloaded, im)
|
||||
reloaded_rgb = reloaded.convert("RGB")
|
||||
|
||||
assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
|
||||
assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
|
||||
|
||||
|
||||
def test_palette_save_duplicate_entries(tmp_path):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import sys
|
||||
from io import StringIO
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
from PIL import Image, IptcImagePlugin
|
||||
|
||||
|
@ -30,6 +30,36 @@ def test_getiptcinfo_jpg_found():
|
|||
assert iptc[(2, 101)] == b"Hungary"
|
||||
|
||||
|
||||
def test_getiptcinfo_fotostation():
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb") as fp:
|
||||
data = bytearray(fp.read())
|
||||
data[86] = 240
|
||||
f = BytesIO(data)
|
||||
with Image.open(f) as im:
|
||||
# Act
|
||||
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||
|
||||
# Assert
|
||||
for tag in iptc.keys():
|
||||
if tag[0] == 240:
|
||||
return
|
||||
assert False, "FotoStation tag not found"
|
||||
|
||||
|
||||
def test_getiptcinfo_zero_padding():
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.info["photoshop"][0x0404] += b"\x00\x00\x00"
|
||||
|
||||
# Act
|
||||
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||
|
||||
# Assert
|
||||
assert isinstance(iptc, dict)
|
||||
assert len(iptc) == 3
|
||||
|
||||
|
||||
def test_getiptcinfo_tiff_none():
|
||||
# Arrange
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
|
|
|
@ -214,13 +214,20 @@ class TestFileJpeg:
|
|||
# Should not raise OSError for image with icc larger than image size.
|
||||
im.save(
|
||||
f,
|
||||
format="JPEG",
|
||||
progressive=True,
|
||||
quality=95,
|
||||
icc_profile=icc_profile,
|
||||
optimize=True,
|
||||
)
|
||||
|
||||
with Image.open("Tests/images/flower2.jpg") as im:
|
||||
f = str(tmp_path / "temp2.jpg")
|
||||
im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955)
|
||||
|
||||
with Image.open("Tests/images/flower2.jpg") as im:
|
||||
f = str(tmp_path / "temp3.jpg")
|
||||
im.save(f, progressive=True, quality=94, exif=b" " * 43668)
|
||||
|
||||
def test_optimize(self):
|
||||
im1 = self.roundtrip(hopper())
|
||||
im2 = self.roundtrip(hopper(), optimize=0)
|
||||
|
@ -945,11 +952,10 @@ class TestFileJpeg:
|
|||
assert repr_jpeg.format == "JPEG"
|
||||
assert_image_similar(im, repr_jpeg, 17)
|
||||
|
||||
def test_repr_jpeg_error(self):
|
||||
def test_repr_jpeg_error_returns_none(self):
|
||||
im = hopper("F")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im._repr_jpeg_()
|
||||
assert im._repr_jpeg_() is None
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_win32(), reason="Windows only")
|
||||
|
|
|
@ -274,17 +274,15 @@ def test_sgnd(tmp_path):
|
|||
assert reloaded_signed.getpixel((0, 0)) == 128
|
||||
|
||||
|
||||
def test_rgba():
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_rgba(ext):
|
||||
# Arrange
|
||||
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
|
||||
with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2:
|
||||
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
|
||||
# Act
|
||||
j2k.load()
|
||||
jp2.load()
|
||||
im.load()
|
||||
|
||||
# Assert
|
||||
assert j2k.mode == "RGBA"
|
||||
assert jp2.mode == "RGBA"
|
||||
assert im.mode == "RGBA"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
|
|
|
@ -8,7 +8,7 @@ from collections import namedtuple
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features
|
||||
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
|
||||
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||
|
||||
from .helper import (
|
||||
|
@ -1035,7 +1035,18 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||
for i in range(2, 9):
|
||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||
assert 274 in im.tag_v2
|
||||
|
||||
im.load()
|
||||
assert 274 not in im.tag_v2
|
||||
|
||||
assert_image_similar(base_im, im, 0.7)
|
||||
|
||||
def test_exif_transpose(self):
|
||||
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||
for i in range(2, 9):
|
||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||
im = ImageOps.exif_transpose(im)
|
||||
|
||||
assert_image_similar(base_im, im, 0.7)
|
||||
|
||||
|
|
|
@ -43,8 +43,25 @@ def test_save(tmp_path, mode):
|
|||
|
||||
|
||||
@skip_unless_feature("jpg_2000")
|
||||
def test_save_rgba(tmp_path):
|
||||
helper_save_as_pdf(tmp_path, "RGBA")
|
||||
@pytest.mark.parametrize("mode", ("LA", "RGBA"))
|
||||
def test_save_alpha(tmp_path, mode):
|
||||
helper_save_as_pdf(tmp_path, mode)
|
||||
|
||||
|
||||
def test_p_alpha(tmp_path):
|
||||
# Arrange
|
||||
outfile = str(tmp_path / "temp.pdf")
|
||||
with Image.open("Tests/images/pil123p.png") as im:
|
||||
assert im.mode == "P"
|
||||
assert isinstance(im.info["transparency"], bytes)
|
||||
|
||||
# Act
|
||||
im.save(outfile)
|
||||
|
||||
# Assert
|
||||
with open(outfile, "rb") as fp:
|
||||
contents = fp.read()
|
||||
assert b"\n/SMask " in contents
|
||||
|
||||
|
||||
def test_monochrome(tmp_path):
|
||||
|
@ -57,8 +74,8 @@ def test_monochrome(tmp_path):
|
|||
|
||||
|
||||
def test_unsupported_mode(tmp_path):
|
||||
im = hopper("LA")
|
||||
outfile = str(tmp_path / "temp_LA.pdf")
|
||||
im = hopper("PA")
|
||||
outfile = str(tmp_path / "temp_PA.pdf")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.save(outfile)
|
||||
|
|
|
@ -79,7 +79,7 @@ class TestFilePng:
|
|||
|
||||
def test_sanity(self, tmp_path):
|
||||
# internal version number
|
||||
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
|
||||
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
|
||||
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
|
@ -92,11 +92,11 @@ class TestFilePng:
|
|||
assert im.format == "PNG"
|
||||
assert im.get_format_mimetype() == "image/png"
|
||||
|
||||
for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
|
||||
for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
|
||||
im = hopper(mode)
|
||||
im.save(test_file)
|
||||
with Image.open(test_file) as reloaded:
|
||||
if mode == "I;16":
|
||||
if mode in ("I;16", "I;16B"):
|
||||
reloaded = reloaded.convert(mode)
|
||||
assert_image_equal(reloaded, im)
|
||||
|
||||
|
@ -532,11 +532,10 @@ class TestFilePng:
|
|||
assert repr_png.format == "PNG"
|
||||
assert_image_equal(im, repr_png)
|
||||
|
||||
def test_repr_png_error(self):
|
||||
def test_repr_png_error_returns_none(self):
|
||||
im = hopper("F")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im._repr_png_()
|
||||
assert im._repr_png_() is None
|
||||
|
||||
def test_chunk_order(self, tmp_path):
|
||||
with Image.open("Tests/images/icc_profile.png") as im:
|
||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
|||
|
||||
from PIL import Image, QoiImagePlugin
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar_tofile
|
||||
from .helper import assert_image_equal_tofile
|
||||
|
||||
|
||||
def test_sanity():
|
||||
|
@ -18,7 +18,7 @@ def test_sanity():
|
|||
assert im.size == (162, 150)
|
||||
assert im.format == "QOI"
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03)
|
||||
assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
|
||||
|
||||
|
||||
def test_invalid_file():
|
||||
|
|
|
@ -235,3 +235,13 @@ class TestFileWebp:
|
|||
with Image.open(out_webp) as reloaded:
|
||||
reloaded.load()
|
||||
assert reloaded.info["duration"] == 1000
|
||||
|
||||
def test_roundtrip_rgba_palette(self, tmp_path):
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
im = Image.new("RGBA", (1, 1)).convert("P")
|
||||
assert im.mode == "P"
|
||||
assert im.palette.mode == "RGBA"
|
||||
im.save(temp_file)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
|
|
|
@ -135,6 +135,12 @@ class TestImage:
|
|||
with pytest.raises(AttributeError):
|
||||
im.size = (3, 4)
|
||||
|
||||
def test_set_mode(self):
|
||||
im = Image.new("RGB", (1, 1))
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
im.mode = "P"
|
||||
|
||||
def test_invalid_image(self):
|
||||
im = io.BytesIO(b"")
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
|
@ -632,8 +638,8 @@ class TestImage:
|
|||
im.remap_palette(None)
|
||||
|
||||
def test_remap_palette_transparency(self):
|
||||
im = Image.new("P", (1, 2))
|
||||
im.putpixel((0, 1), 1)
|
||||
im = Image.new("P", (1, 2), (0, 0, 0))
|
||||
im.putpixel((0, 1), (255, 0, 0))
|
||||
im.info["transparency"] = 0
|
||||
|
||||
im_remapped = im.remap_palette([1, 0])
|
||||
|
@ -655,15 +661,15 @@ class TestImage:
|
|||
blank_p.palette = None
|
||||
blank_pa.palette = None
|
||||
|
||||
def _make_new(base_image, im, palette_result=None):
|
||||
new_im = base_image._new(im)
|
||||
assert new_im.mode == im.mode
|
||||
assert new_im.size == im.size
|
||||
assert new_im.info == base_image.info
|
||||
def _make_new(base_image, image, palette_result=None):
|
||||
new_image = base_image._new(image.im)
|
||||
assert new_image.mode == image.mode
|
||||
assert new_image.size == image.size
|
||||
assert new_image.info == base_image.info
|
||||
if palette_result is not None:
|
||||
assert new_im.palette.tobytes() == palette_result.tobytes()
|
||||
assert new_image.palette.tobytes() == palette_result.tobytes()
|
||||
else:
|
||||
assert new_im.palette is None
|
||||
assert new_image.palette is None
|
||||
|
||||
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
|
||||
_make_new(im_p, im, None)
|
||||
|
@ -900,6 +906,31 @@ class TestImage:
|
|||
im = Image.new("RGB", size)
|
||||
assert im.tobytes() == b""
|
||||
|
||||
def test_has_transparency_data(self):
|
||||
for mode in ("1", "L", "P", "RGB"):
|
||||
im = Image.new(mode, (1, 1))
|
||||
assert not im.has_transparency_data
|
||||
|
||||
for mode in ("LA", "La", "PA", "RGBA", "RGBa"):
|
||||
im = Image.new(mode, (1, 1))
|
||||
assert im.has_transparency_data
|
||||
|
||||
# P mode with "transparency" info
|
||||
with Image.open("Tests/images/first_frame_transparency.gif") as im:
|
||||
assert "transparency" in im.info
|
||||
assert im.has_transparency_data
|
||||
|
||||
# RGB mode with "transparency" info
|
||||
with Image.open("Tests/images/rgb_trns.png") as im:
|
||||
assert "transparency" in im.info
|
||||
assert im.has_transparency_data
|
||||
|
||||
# P mode with RGBA palette
|
||||
im = Image.new("RGBA", (1, 1)).convert("P")
|
||||
assert im.mode == "P"
|
||||
assert im.palette.mode == "RGBA"
|
||||
assert im.has_transparency_data
|
||||
|
||||
def test_apply_transparency(self):
|
||||
im = Image.new("P", (1, 1))
|
||||
im.putpalette((0, 0, 0, 1, 1, 1))
|
||||
|
|
|
@ -213,6 +213,10 @@ class TestImageGetPixel(AccessTest):
|
|||
def test_basic(self, mode):
|
||||
self.check(mode)
|
||||
|
||||
def test_list(self):
|
||||
im = hopper()
|
||||
assert im.getpixel([0, 0]) == (20, 20, 70)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||
@pytest.mark.parametrize(
|
||||
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
|
||||
|
|
|
@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
|
|||
f = str(tmp_path / "temp.png")
|
||||
|
||||
im_l = im.convert("L")
|
||||
assert im_l.info["transparency"] == 1 # undone
|
||||
assert im_l.info["transparency"] == 0
|
||||
im_l.save(f)
|
||||
|
||||
im_rgb = im.convert("RGB")
|
||||
assert im_rgb.info["transparency"] == (0, 1, 2) # undone
|
||||
assert im_rgb.info["transparency"] == (0, 0, 0)
|
||||
im_rgb.save(f)
|
||||
|
||||
|
||||
|
|
|
@ -23,9 +23,12 @@ from .helper import assert_image_equal, hopper
|
|||
ImageFilter.MinFilter,
|
||||
ImageFilter.ModeFilter,
|
||||
ImageFilter.GaussianBlur,
|
||||
ImageFilter.GaussianBlur(0),
|
||||
ImageFilter.GaussianBlur(5),
|
||||
ImageFilter.GaussianBlur((2, 5)),
|
||||
ImageFilter.BoxBlur(0),
|
||||
ImageFilter.BoxBlur(5),
|
||||
ImageFilter.BoxBlur((2, 5)),
|
||||
ImageFilter.UnsharpMask,
|
||||
ImageFilter.UnsharpMask(10),
|
||||
),
|
||||
|
@ -185,12 +188,21 @@ def test_consistency_5x5(mode):
|
|||
assert_image_equal(source.filter(kernel), reference)
|
||||
|
||||
|
||||
def test_invalid_box_blur_filter():
|
||||
@pytest.mark.parametrize(
|
||||
"radius",
|
||||
(
|
||||
-2,
|
||||
(-2, -2),
|
||||
(-2, 2),
|
||||
(2, -2),
|
||||
),
|
||||
)
|
||||
def test_invalid_box_blur_filter(radius):
|
||||
with pytest.raises(ValueError):
|
||||
ImageFilter.BoxBlur(-2)
|
||||
ImageFilter.BoxBlur(radius)
|
||||
|
||||
im = hopper()
|
||||
box_blur_filter = ImageFilter.BoxBlur(2)
|
||||
box_blur_filter.radius = -2
|
||||
box_blur_filter.radius = radius
|
||||
with pytest.raises(ValueError):
|
||||
im.filter(box_blur_filter)
|
||||
|
|
|
@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
|
|||
im.putpalette(palette, mode)
|
||||
assert im.getpalette() == [1, 2, 3]
|
||||
assert im.palette.colors == {(1, 2, 3, 4): 0}
|
||||
|
||||
|
||||
def test_empty_palette():
|
||||
im = Image.new("P", (1, 1))
|
||||
assert im.getpalette() == []
|
||||
|
||||
|
||||
def test_undefined_palette_index():
|
||||
im = Image.new("P", (1, 1), 3)
|
||||
im.putpalette((1, 2, 3))
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)
|
||||
|
|
|
@ -586,6 +586,18 @@ def test_point(points):
|
|||
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
|
||||
|
||||
|
||||
def test_point_I16():
|
||||
# Arrange
|
||||
im = Image.new("I;16", (1, 1))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
draw.point((0, 0), fill=0x1234)
|
||||
|
||||
# Assert
|
||||
assert im.getpixel((0, 0)) == 0x1234
|
||||
|
||||
|
||||
@pytest.mark.parametrize("points", POINTS)
|
||||
def test_polygon(points):
|
||||
# Arrange
|
||||
|
@ -732,7 +744,7 @@ def test_rectangle_I16(bbox):
|
|||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
draw.rectangle(bbox, fill="black", outline="green")
|
||||
draw.rectangle(bbox, outline=0xFFFF)
|
||||
|
||||
# Assert
|
||||
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
|
||||
|
@ -1326,6 +1338,7 @@ def test_stroke_multiline():
|
|||
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3)
|
||||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
def test_setting_default_font():
|
||||
# Arrange
|
||||
im = Image.new("RGB", (100, 250))
|
||||
|
|
|
@ -136,7 +136,7 @@ class TestImageFile:
|
|||
|
||||
class DummyImageFile(ImageFile.ImageFile):
|
||||
def _open(self):
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
self._size = (1, 1)
|
||||
|
||||
im = DummyImageFile(buf)
|
||||
|
@ -217,7 +217,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100
|
|||
class MockImageFile(ImageFile.ImageFile):
|
||||
def _open(self):
|
||||
self.rawmode = "RGBA"
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
self._size = (200, 200)
|
||||
self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
|
||||
|
||||
|
|
|
@ -141,7 +141,9 @@ def test_I16(font):
|
|||
draw = ImageDraw.Draw(im)
|
||||
|
||||
txt = "Hello World!"
|
||||
draw.text((10, 10), txt, font=font)
|
||||
draw.text((10, 10), txt, fill=0xFFFE, font=font)
|
||||
|
||||
assert im.getpixel((12, 14)) == 0xFFFE
|
||||
|
||||
target = "Tests/images/transparent_background_text_L.png"
|
||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||
|
|
|
@ -75,13 +75,13 @@ def test_pickle_la_mode_with_palette(tmp_path):
|
|||
|
||||
# Act / Assert
|
||||
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
|
||||
im.mode = "LA"
|
||||
im._mode = "LA"
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(im, f, protocol)
|
||||
with open(filename, "rb") as f:
|
||||
loaded_im = pickle.load(f)
|
||||
|
||||
im.mode = "PA"
|
||||
im._mode = "PA"
|
||||
assert im == loaded_im
|
||||
|
||||
|
||||
|
@ -112,6 +112,7 @@ def helper_assert_pickled_font_images(font1, font2):
|
|||
assert_image_equal(im1, im2)
|
||||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
|
||||
def test_pickle_font_string(protocol):
|
||||
# Arrange
|
||||
|
@ -125,6 +126,7 @@ def test_pickle_font_string(protocol):
|
|||
helper_assert_pickled_font_images(font, unpickled_font)
|
||||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
|
||||
def test_pickle_font_file(tmp_path, protocol):
|
||||
# Arrange
|
||||
|
|
0
_custom_build/backend.py
Executable file → Normal file
0
_custom_build/backend.py
Executable file → Normal file
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install libimagequant
|
||||
|
||||
archive=libimagequant-4.2.0
|
||||
archive=libimagequant-4.2.1
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -11,4 +11,3 @@ pushd $archive
|
|||
meson build --prefix=/usr && sudo ninja -C build install
|
||||
|
||||
popd
|
||||
|
||||
|
|
|
@ -15,4 +15,3 @@ make && sudo make install
|
|||
cd ..
|
||||
|
||||
popd
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install webp
|
||||
|
||||
archive=libwebp-1.3.1
|
||||
archive=libwebp-1.3.2
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -2,4 +2,3 @@
|
|||
|
||||
pkg install -y python ndk-sysroot clang make \
|
||||
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
|
||||
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
|
||||
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels
|
||||
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
|
||||
.. _GitHub: https://github.com/python-pillow/Pillow
|
||||
.. _Python Package Index: https://pypi.org/project/Pillow/
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
|
||||
flags, height, width = struct.unpack("<3I", header.read(12))
|
||||
self._size = (width, height)
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
|
||||
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
||||
struct.unpack("<11I", header.read(44)) # reserved
|
||||
|
|
|
@ -63,8 +63,35 @@ DDS
|
|||
^^^
|
||||
|
||||
DDS is a popular container texture format used in video games and natively supported
|
||||
by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1,
|
||||
DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode.
|
||||
by DirectX.
|
||||
|
||||
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
|
||||
|
||||
.. versionadded:: 3.4.0
|
||||
DXT3 images can be read in ``RGB`` mode and DX10 images can be read in
|
||||
``RGB`` and ``RGBA`` mode.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
Uncompressed ``RGBA`` images can be read.
|
||||
|
||||
|
||||
.. versionadded:: 8.3.0
|
||||
BC5S images can be opened in ``RGB`` mode, and uncompressed ``RGB`` images
|
||||
can be read. Uncompressed data can also be saved to image files.
|
||||
|
||||
|
||||
.. versionadded:: 9.3.0
|
||||
ATI1 images can be opened in ``L`` mode and ATI2 images can be opened in
|
||||
``RGB`` mode.
|
||||
|
||||
.. versionadded:: 9.4.0
|
||||
Uncompressed ``L`` ("luminance") and ``LA`` images can be opened and saved.
|
||||
|
||||
|
||||
.. versionadded:: 10.1.0
|
||||
BC5U can be read in ``RGB`` mode, and 8-bit color indexed images can be read
|
||||
in ``P`` mode.
|
||||
|
||||
|
||||
DIB
|
||||
^^^
|
||||
|
@ -88,8 +115,13 @@ in ``L``, ``RGB`` and ``CMYK`` modes.
|
|||
Loading
|
||||
~~~~~~~
|
||||
|
||||
To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it
|
||||
also searches for "gswin32c" and "gswin64c". To customise this behaviour,
|
||||
``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to
|
||||
use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use.
|
||||
|
||||
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
|
||||
method with the following parameters to affect how Ghostscript renders the EPS
|
||||
method with the following parameters to affect how Ghostscript renders the EPS.
|
||||
|
||||
**scale**
|
||||
Affects the scale of the resultant rasterized image. If the EPS suggests
|
||||
|
@ -253,7 +285,7 @@ their :py:attr:`~PIL.Image.Image.info` values.
|
|||
|
||||
**loop**
|
||||
Integer number of times the GIF should loop. 0 means that it will loop
|
||||
forever. By default, the image will not loop.
|
||||
forever. If omitted or ``None``, the image will not loop.
|
||||
|
||||
**comment**
|
||||
A comment about the image.
|
||||
|
@ -861,6 +893,10 @@ PPM
|
|||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
|
||||
``RGB`` data.
|
||||
|
||||
"Raw" (P4 to P6) formats can be read, and are used when writing.
|
||||
|
||||
Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well.
|
||||
|
||||
SGI
|
||||
^^^
|
||||
|
||||
|
@ -1482,7 +1518,7 @@ files. Different encoding methods are used, depending on the image mode.
|
|||
unavailable
|
||||
* L, RGB and CMYK mode images use JPEG encoding
|
||||
* P mode images use HEX encoding
|
||||
* RGBA mode images use JPEG2000 encoding
|
||||
* LA and RGBA mode images use JPEG2000 encoding
|
||||
|
||||
.. _pdf-saving:
|
||||
|
||||
|
|
|
@ -72,11 +72,11 @@ true color.
|
|||
# mode setting
|
||||
bits = int(header[3])
|
||||
if bits == 1:
|
||||
self.mode = "1"
|
||||
self._mode = "1"
|
||||
elif bits == 8:
|
||||
self.mode = "L"
|
||||
self._mode = "L"
|
||||
elif bits == 24:
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
else:
|
||||
msg = "unknown number of bits"
|
||||
raise SyntaxError(msg)
|
||||
|
|
|
@ -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
|
||||
:alt: AppVeyor CI build status (Windows)
|
||||
|
||||
.. image:: https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg
|
||||
:target: https://github.com/python-pillow/pillow-wheels/actions
|
||||
:alt: GitHub Actions wheels build status (Wheels)
|
||||
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
|
||||
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
|
||||
:alt: GitHub Actions build status (Wheels)
|
||||
|
||||
.. image:: https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels
|
||||
:target: https://app.travis-ci.com/github/python-pillow/pillow-wheels
|
||||
.. 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
|
||||
:alt: Travis CI wheels build status (aarch64)
|
||||
|
||||
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
|
||||
|
|
|
@ -83,10 +83,9 @@ Install Pillow with :command:`pip`::
|
|||
.. tab:: Windows
|
||||
|
||||
We provide Pillow binaries for Windows compiled for the matrix of
|
||||
supported Pythons in both 32 and 64-bit versions in the wheel format.
|
||||
These binaries include support for all optional libraries except
|
||||
libimagequant and libxcb. Raqm support requires
|
||||
FriBiDi to be installed separately::
|
||||
supported Pythons in 64-bit versions in the wheel format. These binaries include
|
||||
support for all optional libraries except libimagequant and libxcb. Raqm support
|
||||
requires FriBiDi to be installed separately::
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade Pillow
|
||||
|
@ -181,7 +180,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **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
|
||||
the Pillow license, therefore we will not be distributing binaries
|
||||
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 |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+===========================+==================+==============+
|
||||
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.0 |arm |
|
||||
| +---------------------------+------------------+ |
|
||||
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 | |
|
||||
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
|
||||
| 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 |
|
||||
| +---------------------------+------------------+--------------+
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5
|
||||
Pillow >= 10,Yes,Yes,Yes,Yes,,,
|
||||
Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,,
|
||||
Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,
|
||||
Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,
|
||||
Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,
|
||||
Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes
|
||||
Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5
|
||||
Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,,
|
||||
Pillow 10.0,,Yes,Yes,Yes,Yes,,,
|
||||
Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,,
|
||||
Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,,
|
||||
Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes,
|
||||
Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes,
|
||||
Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes
|
||||
|
|
|
|
@ -93,10 +93,14 @@ Generating images
|
|||
Registering plugins
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autofunction:: preinit
|
||||
.. autofunction:: init
|
||||
|
||||
.. note::
|
||||
|
||||
These functions are for use by plugin authors. Application authors can
|
||||
ignore them.
|
||||
These functions are for use by plugin authors. They are called when a
|
||||
plugin is loaded as part of :py:meth:`~preinit()` or :py:meth:`~init()`.
|
||||
Application authors can ignore them.
|
||||
|
||||
.. autofunction:: register_open
|
||||
.. autofunction:: register_mime
|
||||
|
@ -347,6 +351,8 @@ Instances of the :py:class:`Image` class have the following attributes:
|
|||
|
||||
.. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell`
|
||||
|
||||
.. autoattribute:: PIL.Image.Image.has_transparency_data
|
||||
|
||||
Classes
|
||||
-------
|
||||
|
||||
|
|
|
@ -538,7 +538,7 @@ Methods
|
|||
It should be a `BCP 47 language code`_.
|
||||
Requires libraqm.
|
||||
:param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
|
||||
:return: Width for horizontal, height for vertical text.
|
||||
:return: Either width for horizontal text, or height for vertical text.
|
||||
|
||||
.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
|
||||
|
||||
|
|
|
@ -206,4 +206,4 @@ Support reading signed 8-bit TIFF images
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TIFF images with signed integer data, 8 bits per sample and a photometric
|
||||
interpretaton of BlackIsZero can now be read.
|
||||
interpretation of BlackIsZero can now be read.
|
||||
|
|
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
|
||||
with an error. This behaviour can be disabled with the ``--disable-libjpeg``
|
||||
and ``--disable-zlib`` flags.
|
||||
|
||||
|
|
|
@ -34,7 +34,3 @@ image size can lead to a smaller allocation than expected, leading to
|
|||
arbitrary writes.
|
||||
|
||||
This issue was found by Cris Neckar at Divergent Security.
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -20,5 +20,3 @@ CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix
|
|||
undefines PySlice_GetIndicesEx if it exists to restore compatibility
|
||||
with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for
|
||||
more details.
|
||||
|
||||
|
||||
|
|
|
@ -8,4 +8,3 @@ Fixed Windows PyPy Build
|
|||
|
||||
A change in the 4.2.0 cycle broke the Windows PyPy build. This has
|
||||
been fixed, and PyPy is now part of the Windows CI matrix.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
10.1.0
|
||||
10.0.1
|
||||
10.0.0
|
||||
9.5.0
|
||||
9.4.0
|
||||
|
|
|
@ -16,6 +16,7 @@ classifiers =
|
|||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
Programming Language :: Python :: Implementation :: PyPy
|
||||
Topic :: Multimedia :: Graphics
|
||||
|
|
2
setup.py
2
setup.py
|
@ -39,7 +39,7 @@ TIFF_ROOT = None
|
|||
ZLIB_ROOT = None
|
||||
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
|
||||
|
||||
if sys.platform == "win32" and sys.version_info >= (3, 12):
|
||||
if sys.platform == "win32" and sys.version_info >= (3, 13):
|
||||
import atexit
|
||||
|
||||
atexit.register(
|
||||
|
|
|
@ -68,11 +68,11 @@ def bdf_char(f):
|
|||
# followed by the width in x (BBw), height in y (BBh),
|
||||
# and x and y displacement (BBxoff0, BByoff0)
|
||||
# of the lower left corner from the origin of the character.
|
||||
width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()]
|
||||
width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
|
||||
|
||||
# The word DWIDTH
|
||||
# followed by the width in x and y of the character in device pixels.
|
||||
dwx, dwy = [int(p) for p in props["DWIDTH"].split()]
|
||||
dwx, dwy = (int(p) for p in props["DWIDTH"].split())
|
||||
|
||||
bbox = (
|
||||
(dwx, dwy),
|
||||
|
|
|
@ -266,7 +266,7 @@ class BlpImageFile(ImageFile.ImageFile):
|
|||
msg = f"Bad BLP magic {repr(self.magic)}"
|
||||
raise BLPFormatError(msg)
|
||||
|
||||
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
|
||||
self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
|
||||
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
|
||||
|
||||
|
||||
|
@ -419,9 +419,11 @@ class BLPEncoder(ImageFile.PyEncoder):
|
|||
def _write_palette(self):
|
||||
data = b""
|
||||
palette = self.im.getpalette("RGBA", "RGBA")
|
||||
for i in range(256):
|
||||
for i in range(len(palette) // 4):
|
||||
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
||||
data += struct.pack("<4B", b, g, r, a)
|
||||
while len(data) < 256 * 4:
|
||||
data += b"\x00" * 4
|
||||
return data
|
||||
|
||||
def encode(self, bufsize):
|
||||
|
@ -442,7 +444,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
|||
return len(data), 0, data
|
||||
|
||||
|
||||
def _save(im, fp, filename, save_all=False):
|
||||
def _save(im, fp, filename):
|
||||
if im.mode != "P":
|
||||
msg = "Unsupported BLP image mode"
|
||||
raise ValueError(msg)
|
||||
|
|
|
@ -163,7 +163,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
offset += 4 * file_info["colors"]
|
||||
|
||||
# ---------------------- Check bit depth for unusual unsupported values
|
||||
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
|
||||
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
|
||||
if self.mode is None:
|
||||
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
|
||||
raise OSError(msg)
|
||||
|
@ -200,7 +200,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
|
||||
):
|
||||
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
|
||||
self.mode = "RGBA" if "A" in raw_mode else self.mode
|
||||
self._mode = "RGBA" if "A" in raw_mode else self.mode
|
||||
elif (
|
||||
file_info["bits"] in (24, 16)
|
||||
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
|
||||
|
@ -214,7 +214,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
raise OSError(msg)
|
||||
elif file_info["compression"] == self.RAW:
|
||||
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
|
||||
raw_mode, self.mode = "BGRA", "RGBA"
|
||||
raw_mode, self._mode = "BGRA", "RGBA"
|
||||
elif file_info["compression"] in (self.RLE8, self.RLE4):
|
||||
decoder_name = "bmp_rle"
|
||||
else:
|
||||
|
@ -245,10 +245,10 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
|
||||
# ------- If all colors are grey, white or black, ditch palette
|
||||
if greyscale:
|
||||
self.mode = "1" if file_info["colors"] == 2 else "L"
|
||||
self._mode = "1" if file_info["colors"] == 2 else "L"
|
||||
raw_mode = self.mode
|
||||
else:
|
||||
self.mode = "P"
|
||||
self._mode = "P"
|
||||
self.palette = ImagePalette.raw(
|
||||
"BGRX" if padding == 4 else "BGR", palette
|
||||
)
|
||||
|
|
|
@ -46,7 +46,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
|
|||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self._mode = "F"
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
|
|
|
@ -13,7 +13,7 @@ Full text of the CC0 license:
|
|||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from . import Image, ImageFile
|
||||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import o32le as o32
|
||||
|
||||
# Magic ("DDS ")
|
||||
|
@ -128,7 +128,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
|
||||
flags, height, width = struct.unpack("<3I", header.read(12))
|
||||
self._size = (width, height)
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
|
||||
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
|
||||
struct.unpack("<11I", header.read(44)) # reserved
|
||||
|
@ -141,9 +141,9 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
if pfflags & DDPF_LUMINANCE:
|
||||
# Texture contains uncompressed L or LA data
|
||||
if pfflags & DDPF_ALPHAPIXELS:
|
||||
self.mode = "LA"
|
||||
self._mode = "LA"
|
||||
else:
|
||||
self.mode = "L"
|
||||
self._mode = "L"
|
||||
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))]
|
||||
elif pfflags & DDPF_RGB:
|
||||
|
@ -153,10 +153,14 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
if pfflags & DDPF_ALPHAPIXELS:
|
||||
rawmode += masks[0xFF000000]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
|
||||
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
|
||||
elif pfflags & DDPF_PALETTEINDEXED8:
|
||||
self._mode = "P"
|
||||
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, "L")]
|
||||
else:
|
||||
data_start = header_size + 4
|
||||
n = 0
|
||||
|
@ -172,15 +176,15 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
elif fourcc == b"ATI1":
|
||||
self.pixel_format = "BC4"
|
||||
n = 4
|
||||
self.mode = "L"
|
||||
elif fourcc == b"ATI2":
|
||||
self._mode = "L"
|
||||
elif fourcc in (b"ATI2", b"BC5U"):
|
||||
self.pixel_format = "BC5"
|
||||
n = 5
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
elif fourcc == b"BC5S":
|
||||
self.pixel_format = "BC5S"
|
||||
n = 5
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
elif fourcc == b"DX10":
|
||||
data_start += 20
|
||||
# ignoring flags which pertain to volume textures and cubemaps
|
||||
|
@ -189,19 +193,19 @@ class DdsImageFile(ImageFile.ImageFile):
|
|||
if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
|
||||
self.pixel_format = "BC5"
|
||||
n = 5
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
|
||||
self.pixel_format = "BC5S"
|
||||
n = 5
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
|
||||
self.pixel_format = "BC6H"
|
||||
n = 6
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
|
||||
self.pixel_format = "BC6HS"
|
||||
n = 6
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
|
||||
self.pixel_format = "BC7"
|
||||
n = 7
|
||||
|
|
|
@ -37,8 +37,15 @@ from ._deprecate import deprecate
|
|||
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
||||
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
||||
|
||||
gs_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 gs_windows_binary is None:
|
||||
import shutil
|
||||
|
||||
for binary in ("gswin32c", "gswin64c", "gs"):
|
||||
|
@ -47,23 +54,22 @@ if sys.platform.startswith("win"):
|
|||
break
|
||||
else:
|
||||
gs_windows_binary = False
|
||||
|
||||
|
||||
def has_ghostscript():
|
||||
if gs_windows_binary:
|
||||
return True
|
||||
if not sys.platform.startswith("win"):
|
||||
gs_binary = gs_windows_binary
|
||||
else:
|
||||
try:
|
||||
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
||||
return True
|
||||
gs_binary = "gs"
|
||||
except OSError:
|
||||
# No Ghostscript
|
||||
pass
|
||||
return False
|
||||
gs_binary = False
|
||||
return gs_binary is not False
|
||||
|
||||
|
||||
def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||
"""Render an image using Ghostscript"""
|
||||
global gs_binary
|
||||
if not has_ghostscript():
|
||||
msg = "Unable to locate Ghostscript on paths"
|
||||
raise OSError(msg)
|
||||
|
||||
# Unpack decoder tile
|
||||
decoder, tile, offset, data = tile[0]
|
||||
|
@ -113,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
|||
|
||||
# Build Ghostscript command
|
||||
command = [
|
||||
"gs",
|
||||
gs_binary,
|
||||
"-q", # quiet mode
|
||||
"-g%dx%d" % size, # set output geometry (pixels)
|
||||
"-r%fx%f" % res, # set input DPI (dots per inch)
|
||||
|
@ -132,19 +138,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
|||
"showpage",
|
||||
]
|
||||
|
||||
if gs_windows_binary is not None:
|
||||
if not gs_windows_binary:
|
||||
try:
|
||||
os.unlink(outfile)
|
||||
if infile_temp:
|
||||
os.unlink(infile_temp)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
msg = "Unable to locate Ghostscript on paths"
|
||||
raise OSError(msg)
|
||||
command[0] = gs_windows_binary
|
||||
|
||||
# push data through Ghostscript
|
||||
try:
|
||||
startupinfo = None
|
||||
|
@ -227,13 +220,15 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# go to offset - start of "%!PS"
|
||||
self.fp.seek(offset)
|
||||
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
self._size = None
|
||||
|
||||
byte_arr = bytearray(255)
|
||||
bytes_mv = memoryview(byte_arr)
|
||||
bytes_read = 0
|
||||
reading_comments = True
|
||||
reading_header_comments = True
|
||||
reading_trailer_comments = False
|
||||
trailer_reached = False
|
||||
|
||||
def check_required_header_comments():
|
||||
if "PS-Adobe" not in self.info:
|
||||
|
@ -243,6 +238,36 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
msg = 'EPS header missing "%%BoundingBox" comment'
|
||||
raise SyntaxError(msg)
|
||||
|
||||
def _read_comment(s):
|
||||
nonlocal reading_trailer_comments
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as e:
|
||||
msg = "not an EPS file"
|
||||
raise SyntaxError(msg) from e
|
||||
|
||||
if m:
|
||||
k, v = m.group(1, 2)
|
||||
self.info[k] = v
|
||||
if k == "BoundingBox":
|
||||
if v == "(atend)":
|
||||
reading_trailer_comments = True
|
||||
elif not self._size or (
|
||||
trailer_reached and reading_trailer_comments
|
||||
):
|
||||
try:
|
||||
# Note: The DSC spec says that BoundingBox
|
||||
# fields should be integers, but some drivers
|
||||
# put floating point values there anyway.
|
||||
box = [int(float(i)) for i in v.split()]
|
||||
self._size = box[2] - box[0], box[3] - box[1]
|
||||
self.tile = [
|
||||
("eps", (0, 0) + self.size, offset, (length, box))
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
while True:
|
||||
byte = self.fp.read(1)
|
||||
if byte == b"":
|
||||
|
@ -265,9 +290,9 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
msg = "not an EPS file"
|
||||
raise SyntaxError(msg)
|
||||
else:
|
||||
if reading_comments:
|
||||
if reading_header_comments:
|
||||
check_required_header_comments()
|
||||
reading_comments = False
|
||||
reading_header_comments = False
|
||||
# reset bytes_read so we can keep reading
|
||||
# data until the end of the line
|
||||
bytes_read = 0
|
||||
|
@ -275,7 +300,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
bytes_read += 1
|
||||
continue
|
||||
|
||||
if reading_comments:
|
||||
if reading_header_comments:
|
||||
# Load EPS header
|
||||
|
||||
# if this line doesn't start with a "%",
|
||||
|
@ -283,33 +308,11 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# then we've reached the end of the header/comments
|
||||
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
|
||||
check_required_header_comments()
|
||||
reading_comments = False
|
||||
reading_header_comments = False
|
||||
continue
|
||||
|
||||
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||
|
||||
try:
|
||||
m = split.match(s)
|
||||
except re.error as e:
|
||||
msg = "not an EPS file"
|
||||
raise SyntaxError(msg) from e
|
||||
|
||||
if m:
|
||||
k, v = m.group(1, 2)
|
||||
self.info[k] = v
|
||||
if k == "BoundingBox":
|
||||
try:
|
||||
# Note: The DSC spec says that BoundingBox
|
||||
# fields should be integers, but some drivers
|
||||
# put floating point values there anyway.
|
||||
box = [int(float(i)) for i in v.split()]
|
||||
self._size = box[2] - box[0], box[3] - box[1]
|
||||
self.tile = [
|
||||
("eps", (0, 0) + self.size, offset, (length, box))
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if not _read_comment(s):
|
||||
m = field.match(s)
|
||||
if m:
|
||||
k = m.group(1)
|
||||
|
@ -339,15 +342,15 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# data start identifier (the image data follows after a single line
|
||||
# consisting only of this quoted value)
|
||||
image_data_values = byte_arr[11:bytes_read].split(None, 7)
|
||||
columns, rows, bit_depth, mode_id = [
|
||||
columns, rows, bit_depth, mode_id = (
|
||||
int(value) for value in image_data_values[:4]
|
||||
]
|
||||
)
|
||||
|
||||
if bit_depth == 1:
|
||||
self.mode = "1"
|
||||
self._mode = "1"
|
||||
elif bit_depth == 8:
|
||||
try:
|
||||
self.mode = self.mode_map[mode_id]
|
||||
self._mode = self.mode_map[mode_id]
|
||||
except ValueError:
|
||||
break
|
||||
else:
|
||||
|
@ -355,7 +358,18 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
|
||||
self._size = columns, rows
|
||||
return
|
||||
elif trailer_reached and reading_trailer_comments:
|
||||
# Load EPS trailer
|
||||
|
||||
# if this line starts with "%%EOF",
|
||||
# then we've reached the end of the file
|
||||
if bytes_mv[:5] == b"%%EOF":
|
||||
break
|
||||
|
||||
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||
_read_comment(s)
|
||||
elif bytes_mv[:9] == b"%%Trailer":
|
||||
trailer_reached = True
|
||||
bytes_read = 0
|
||||
|
||||
check_required_header_comments()
|
||||
|
@ -391,7 +405,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
|||
# Load EPS via Ghostscript
|
||||
if self.tile:
|
||||
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
|
||||
self.mode = self.im.mode
|
||||
self._mode = self.im.mode
|
||||
self._size = self.im.size
|
||||
self.tile = []
|
||||
return Image.Image.load(self)
|
||||
|
|
|
@ -51,14 +51,14 @@ class FitsImageFile(ImageFile.ImageFile):
|
|||
|
||||
number_of_bits = int(headers[b"BITPIX"])
|
||||
if number_of_bits == 8:
|
||||
self.mode = "L"
|
||||
self._mode = "L"
|
||||
elif number_of_bits == 16:
|
||||
self.mode = "I"
|
||||
self._mode = "I"
|
||||
# rawmode = "I;16S"
|
||||
elif number_of_bits == 32:
|
||||
self.mode = "I"
|
||||
self._mode = "I"
|
||||
elif number_of_bits in (-32, -64):
|
||||
self.mode = "F"
|
||||
self._mode = "F"
|
||||
# rawmode = "F" if number_of_bits == -32 else "F;64F"
|
||||
|
||||
offset = math.ceil(self.fp.tell() / 2880) * 2880
|
||||
|
|
|
@ -56,7 +56,7 @@ class FliImageFile(ImageFile.ImageFile):
|
|||
self.is_animated = self.n_frames > 1
|
||||
|
||||
# image characteristics
|
||||
self.mode = "P"
|
||||
self._mode = "P"
|
||||
self._size = i16(s, 8), i16(s, 10)
|
||||
|
||||
# animation speed
|
||||
|
|
|
@ -106,7 +106,7 @@ class FpxImageFile(ImageFile.ImageFile):
|
|||
# note: for now, we ignore the "uncalibrated" flag
|
||||
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
|
||||
|
||||
self.mode, self.rawmode = MODES[tuple(colors)]
|
||||
self._mode, self.rawmode = MODES[tuple(colors)]
|
||||
|
||||
# load JPEG tables, if any
|
||||
self.jpeg = {}
|
||||
|
|
|
@ -77,7 +77,7 @@ class FtexImageFile(ImageFile.ImageFile):
|
|||
self._size = struct.unpack("<2i", self.fp.read(8))
|
||||
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
|
||||
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
|
||||
# Only support single-format files.
|
||||
# I don't know of any multi-format file.
|
||||
|
@ -90,7 +90,7 @@ class FtexImageFile(ImageFile.ImageFile):
|
|||
data = self.fp.read(mipmap_size)
|
||||
|
||||
if format == Format.DXT1:
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
self.tile = [("bcn", (0, 0) + self.size, 0, 1)]
|
||||
elif format == Format.UNCOMPRESSED:
|
||||
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
|
||||
|
|
|
@ -73,9 +73,9 @@ class GbrImageFile(ImageFile.ImageFile):
|
|||
comment = self.fp.read(comment_length)[:-1]
|
||||
|
||||
if color_depth == 1:
|
||||
self.mode = "L"
|
||||
self._mode = "L"
|
||||
else:
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
|
||||
self._size = width, height
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class GdImageFile(ImageFile.ImageFile):
|
|||
msg = "Not a valid GD 2.x .gd file"
|
||||
raise SyntaxError(msg)
|
||||
|
||||
self.mode = "L" # FIXME: "P"
|
||||
self._mode = "L" # FIXME: "P"
|
||||
self._size = i16(s, 2), i16(s, 4)
|
||||
|
||||
true_color = s[6]
|
||||
|
|
|
@ -304,11 +304,11 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
if frame == 0:
|
||||
if self._frame_palette:
|
||||
if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
|
||||
self.mode = "RGBA" if frame_transparency is not None else "RGB"
|
||||
self._mode = "RGBA" if frame_transparency is not None else "RGB"
|
||||
else:
|
||||
self.mode = "P"
|
||||
self._mode = "P"
|
||||
else:
|
||||
self.mode = "L"
|
||||
self._mode = "L"
|
||||
|
||||
if not palette and self.global_palette:
|
||||
from copy import copy
|
||||
|
@ -325,10 +325,10 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
if "transparency" in self.info:
|
||||
self.im.putpalettealpha(self.info["transparency"], 0)
|
||||
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
del self.info["transparency"]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
|
||||
def _rgb(color):
|
||||
|
@ -424,7 +424,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self.im.putpalette(*self._frame_palette.getdata())
|
||||
else:
|
||||
self.im = None
|
||||
self.mode = temp_mode
|
||||
self._mode = temp_mode
|
||||
self._frame_palette = None
|
||||
|
||||
super().load_prepare()
|
||||
|
@ -434,9 +434,9 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
|
||||
if self._frame_transparency is not None:
|
||||
self.im.putpalettealpha(self._frame_transparency, 0)
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self._mode = "RGB"
|
||||
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
|
||||
return
|
||||
if not self._prev_im:
|
||||
|
@ -449,7 +449,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
frame_im = self._crop(frame_im, self.dispose_extent)
|
||||
|
||||
self.im = self._prev_im
|
||||
self.mode = self.im.mode
|
||||
self._mode = self.im.mode
|
||||
if frame_im.mode == "RGBA":
|
||||
self.im.paste(frame_im, self.dispose_extent, frame_im)
|
||||
else:
|
||||
|
@ -683,11 +683,7 @@ def get_interlace(im):
|
|||
def _write_local_header(fp, im, offset, flags):
|
||||
transparent_color_exists = False
|
||||
try:
|
||||
if "transparency" in im.encoderinfo:
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
else:
|
||||
transparency = im.info["transparency"]
|
||||
transparency = int(transparency)
|
||||
transparency = int(im.encoderinfo["transparency"])
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
else:
|
||||
|
@ -916,7 +912,7 @@ def _get_global_header(im, info):
|
|||
info
|
||||
and (
|
||||
"transparency" in info
|
||||
or "loop" in info
|
||||
or info.get("loop") is not None
|
||||
or info.get("duration")
|
||||
or info.get("comment")
|
||||
)
|
||||
|
@ -941,7 +937,7 @@ def _get_global_header(im, info):
|
|||
# Global Color Table
|
||||
_get_header_palette(palette_bytes),
|
||||
]
|
||||
if "loop" in info:
|
||||
if info.get("loop") is not None:
|
||||
header.append(
|
||||
b"!"
|
||||
+ o8(255) # extension intro
|
||||
|
|
|
@ -46,7 +46,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
|
|||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self._mode = "F"
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
|
|
|
@ -46,7 +46,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
|
|||
self.fp.seek(offset)
|
||||
|
||||
# make something up
|
||||
self.mode = "F"
|
||||
self._mode = "F"
|
||||
self._size = 1, 1
|
||||
|
||||
loader = self._load()
|
||||
|
|
|
@ -253,7 +253,7 @@ class IcnsImageFile(ImageFile.ImageFile):
|
|||
|
||||
def _open(self):
|
||||
self.icns = IcnsFile(self.fp)
|
||||
self.mode = "RGBA"
|
||||
self._mode = "RGBA"
|
||||
self.info["sizes"] = self.icns.itersizes()
|
||||
self.best_size = self.icns.bestsize()
|
||||
self.size = (
|
||||
|
@ -305,7 +305,7 @@ class IcnsImageFile(ImageFile.ImageFile):
|
|||
px = im.load()
|
||||
|
||||
self.im = im.im
|
||||
self.mode = im.mode
|
||||
self._mode = im.mode
|
||||
self.size = im.size
|
||||
|
||||
return px
|
||||
|
|
|
@ -330,7 +330,7 @@ class IcoImageFile(ImageFile.ImageFile):
|
|||
im.load()
|
||||
self.im = im.im
|
||||
self.pyaccess = None
|
||||
self.mode = im.mode
|
||||
self._mode = im.mode
|
||||
if im.size != self.size:
|
||||
warnings.warn("Image was not the expected size")
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
|
||||
# Basic attributes
|
||||
self._size = self.info[SIZE]
|
||||
self.mode = self.info[MODE]
|
||||
self._mode = self.info[MODE]
|
||||
|
||||
# Skip forward to start of image data
|
||||
while s and s[:1] != b"\x1A":
|
||||
|
@ -231,9 +231,9 @@ class ImImageFile(ImageFile.ImageFile):
|
|||
self.lut = list(palette[:256])
|
||||
else:
|
||||
if self.mode in ["L", "P"]:
|
||||
self.mode = self.rawmode = "P"
|
||||
self._mode = self.rawmode = "P"
|
||||
elif self.mode in ["LA", "PA"]:
|
||||
self.mode = "PA"
|
||||
self._mode = "PA"
|
||||
self.rawmode = "PA;L"
|
||||
self.palette = ImagePalette.raw("RGB;L", palette)
|
||||
elif self.mode == "RGB":
|
||||
|
|
|
@ -298,7 +298,11 @@ _initialized = 0
|
|||
|
||||
|
||||
def preinit():
|
||||
"""Explicitly load standard file format drivers."""
|
||||
"""
|
||||
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.
|
||||
|
||||
It is called when opening or saving images.
|
||||
"""
|
||||
|
||||
global _initialized
|
||||
if _initialized >= 1:
|
||||
|
@ -334,11 +338,6 @@ def preinit():
|
|||
assert PngImagePlugin
|
||||
except ImportError:
|
||||
pass
|
||||
# try:
|
||||
# import TiffImagePlugin
|
||||
# assert TiffImagePlugin
|
||||
# except ImportError:
|
||||
# pass
|
||||
|
||||
_initialized = 1
|
||||
|
||||
|
@ -347,6 +346,9 @@ def init():
|
|||
"""
|
||||
Explicitly initializes the Python Imaging Library. This function
|
||||
loads all available file format drivers.
|
||||
|
||||
It is called when opening or saving images if :py:meth:`~preinit()` is
|
||||
insufficient, and by :py:meth:`~PIL.features.pilinfo`.
|
||||
"""
|
||||
|
||||
global _initialized
|
||||
|
@ -482,7 +484,7 @@ class Image:
|
|||
# FIXME: take "new" parameters / other image?
|
||||
# FIXME: turn mode and size into delegating properties?
|
||||
self.im = None
|
||||
self.mode = ""
|
||||
self._mode = ""
|
||||
self._size = (0, 0)
|
||||
self.palette = None
|
||||
self.info = {}
|
||||
|
@ -502,10 +504,14 @@ class Image:
|
|||
def size(self):
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
def _new(self, im):
|
||||
new = Image()
|
||||
new.im = im
|
||||
new.mode = im.mode
|
||||
new._mode = im.mode
|
||||
new._size = im.size
|
||||
if im.mode in ("P", "PA"):
|
||||
if self.palette:
|
||||
|
@ -641,9 +647,8 @@ class Image:
|
|||
b = io.BytesIO()
|
||||
try:
|
||||
self.save(b, image_format, **kwargs)
|
||||
except Exception as e:
|
||||
msg = f"Could not save to {image_format} for display"
|
||||
raise ValueError(msg) from e
|
||||
except Exception:
|
||||
return None
|
||||
return b.getvalue()
|
||||
|
||||
def _repr_png_(self):
|
||||
|
@ -693,7 +698,7 @@ class Image:
|
|||
Image.__init__(self)
|
||||
info, mode, size, palette, data = state
|
||||
self.info = info
|
||||
self.mode = mode
|
||||
self._mode = mode
|
||||
self._size = size
|
||||
self.im = core.new(mode, size)
|
||||
if mode in ("L", "LA", "P", "PA") and palette:
|
||||
|
@ -910,7 +915,7 @@ class Image:
|
|||
|
||||
self.load()
|
||||
|
||||
has_transparency = self.info.get("transparency") is not None
|
||||
has_transparency = "transparency" in self.info
|
||||
if not mode and self.mode == "P":
|
||||
# determine default mode
|
||||
if self.palette:
|
||||
|
@ -1069,7 +1074,7 @@ class Image:
|
|||
if mode == "P" and palette != Palette.ADAPTIVE:
|
||||
from . import ImagePalette
|
||||
|
||||
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
||||
new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
|
||||
if delete_trns:
|
||||
# crash fail if we leave a bytes transparency in an rgb/l mode.
|
||||
del new_im.info["transparency"]
|
||||
|
@ -1526,6 +1531,24 @@ class Image:
|
|||
rawmode = mode
|
||||
return list(self.im.getpalette(mode, rawmode))
|
||||
|
||||
@property
|
||||
def has_transparency_data(self) -> bool:
|
||||
"""
|
||||
Determine if an image has transparency data, whether in the form of an
|
||||
alpha channel, a palette with an alpha channel, or a "transparency" key
|
||||
in the info dictionary.
|
||||
|
||||
Note the image might still appear solid, if all of the values shown
|
||||
within are opaque.
|
||||
|
||||
:returns: A boolean.
|
||||
"""
|
||||
return (
|
||||
self.mode in ("LA", "La", "PA", "RGBA", "RGBa")
|
||||
or (self.mode == "P" and self.palette.mode.endswith("A"))
|
||||
or "transparency" in self.info
|
||||
)
|
||||
|
||||
def apply_transparency(self):
|
||||
"""
|
||||
If a P mode image has a "transparency" key in the info dictionary,
|
||||
|
@ -1562,7 +1585,7 @@ class Image:
|
|||
self.load()
|
||||
if self.pyaccess:
|
||||
return self.pyaccess.getpixel(xy)
|
||||
return self.im.getpixel(xy)
|
||||
return self.im.getpixel(tuple(xy))
|
||||
|
||||
def getprojection(self):
|
||||
"""
|
||||
|
@ -1840,7 +1863,7 @@ class Image:
|
|||
raise ValueError from e # sanity check
|
||||
self.im = im
|
||||
self.pyaccess = None
|
||||
self.mode = self.im.mode
|
||||
self._mode = self.im.mode
|
||||
except KeyError as e:
|
||||
msg = "illegal image mode"
|
||||
raise ValueError(msg) from e
|
||||
|
@ -1918,7 +1941,7 @@ class Image:
|
|||
if not isinstance(data, bytes):
|
||||
data = bytes(data)
|
||||
palette = ImagePalette.raw(rawmode, data)
|
||||
self.mode = "PA" if "A" in self.mode else "P"
|
||||
self._mode = "PA" if "A" in self.mode else "P"
|
||||
self.palette = palette
|
||||
self.palette.mode = "RGB"
|
||||
self.load() # install new palette
|
||||
|
@ -2026,7 +2049,7 @@ class Image:
|
|||
mapping_palette = bytearray(new_positions)
|
||||
|
||||
m_im = self.copy()
|
||||
m_im.mode = "P"
|
||||
m_im._mode = "P"
|
||||
|
||||
m_im.palette = ImagePalette.ImagePalette(
|
||||
palette_mode, palette=mapping_palette * bands
|
||||
|
@ -2601,7 +2624,7 @@ class Image:
|
|||
|
||||
self.im = im.im
|
||||
self._size = size
|
||||
self.mode = self.im.mode
|
||||
self._mode = self.im.mode
|
||||
|
||||
self.readonly = 0
|
||||
self.pyaccess = None
|
||||
|
@ -2997,7 +3020,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
|
|||
if args == ():
|
||||
args = mode, 0, 1
|
||||
if args[0] in _MAPMODES:
|
||||
im = new(mode, (1, 1))
|
||||
im = new(mode, (0, 0))
|
||||
im = im._new(core.map_buffer(data, size, decoder_name, 0, args))
|
||||
if mode == "P":
|
||||
from . import ImagePalette
|
||||
|
@ -3404,8 +3427,12 @@ def register_open(id, factory, accept=None):
|
|||
|
||||
def register_mime(id, mimetype):
|
||||
"""
|
||||
Registers an image MIME type. This function should not be used
|
||||
in application code.
|
||||
Registers an image MIME type by populating ``Image.MIME``. This function
|
||||
should not be used in application code.
|
||||
|
||||
``Image.MIME`` provides a mapping from image format identifiers to mime
|
||||
formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can
|
||||
provide a different result for specific images.
|
||||
|
||||
:param id: An image format identifier.
|
||||
:param mimetype: The image MIME type for this format.
|
||||
|
|
|
@ -157,7 +157,8 @@ class GaussianBlur(MultibandFilter):
|
|||
approximates a Gaussian kernel. For details on accuracy see
|
||||
<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"
|
||||
|
@ -166,7 +167,12 @@ class GaussianBlur(MultibandFilter):
|
|||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
return image.gaussian_blur(self.radius)
|
||||
xy = self.radius
|
||||
if not isinstance(xy, (tuple, list)):
|
||||
xy = (xy, xy)
|
||||
if xy == (0, 0):
|
||||
return image.copy()
|
||||
return image.gaussian_blur(xy)
|
||||
|
||||
|
||||
class BoxBlur(MultibandFilter):
|
||||
|
@ -176,21 +182,31 @@ class BoxBlur(MultibandFilter):
|
|||
which runs in linear time relative to the size of the image
|
||||
for any radius value.
|
||||
|
||||
:param radius: Size of the box in one direction. Radius 0 does not blur,
|
||||
returns an identical image. Radius 1 takes 1 pixel
|
||||
in each direction, i.e. 9 pixels in total.
|
||||
:param radius: Size of the box in a direction. Either a sequence of two numbers for
|
||||
x and y, or a single number for both.
|
||||
|
||||
Radius 0 does not blur, returns an identical image.
|
||||
Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total.
|
||||
"""
|
||||
|
||||
name = "BoxBlur"
|
||||
|
||||
def __init__(self, radius):
|
||||
if radius < 0:
|
||||
xy = radius
|
||||
if not isinstance(xy, (tuple, list)):
|
||||
xy = (xy, xy)
|
||||
if xy[0] < 0 or xy[1] < 0:
|
||||
msg = "radius must be >= 0"
|
||||
raise ValueError(msg)
|
||||
self.radius = radius
|
||||
|
||||
def filter(self, image):
|
||||
return image.box_blur(self.radius)
|
||||
xy = self.radius
|
||||
if not isinstance(xy, (tuple, list)):
|
||||
xy = (xy, xy)
|
||||
if xy == (0, 0):
|
||||
return image.copy()
|
||||
return image.box_blur(xy)
|
||||
|
||||
|
||||
class UnsharpMask(MultibandFilter):
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user