Merge branch 'main' into load_default
|
@ -21,13 +21,11 @@ environment:
|
||||||
install:
|
install:
|
||||||
- '%PYTHON%\%EXECUTABLE% --version'
|
- '%PYTHON%\%EXECUTABLE% --version'
|
||||||
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
|
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
|
||||||
- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
|
|
||||||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||||
- 7z x pillow-depends.zip -oc:\
|
|
||||||
- 7z x pillow-test-images.zip -oc:\
|
- 7z x pillow-test-images.zip -oc:\
|
||||||
- mv c:\pillow-depends-main c:\pillow-depends
|
|
||||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||||
- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\
|
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.01-win64.zip
|
||||||
|
- 7z x nasm-win64.zip -oc:\
|
||||||
- choco install ghostscript --version=10.0.0.20230317
|
- choco install ghostscript --version=10.0.0.20230317
|
||||||
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH%
|
||||||
- cd c:\pillow\winbuild\
|
- cd c:\pillow\winbuild\
|
||||||
|
|
|
@ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then
|
||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
||||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||||
sway wl-clipboard
|
sway wl-clipboard libopenblas-dev
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -m pip install --upgrade pip
|
python3 -m pip install --upgrade pip
|
||||||
|
@ -38,11 +38,10 @@ python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
# TODO Remove condition when NumPy supports 3.12
|
python3 -m pip install numpy
|
||||||
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
|
|
||||||
|
|
||||||
# PyQt6 doesn't support PyPy3
|
# PyQt6 doesn't support PyPy3
|
||||||
if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
|
||||||
python3 -m pip install pyqt6
|
python3 -m pip install pyqt6
|
||||||
fi
|
fi
|
||||||
|
|
2
.github/workflows/docs.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
name: Docs
|
name: Docs
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
|
|
2
.github/workflows/lint.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: pre-commit cache
|
- name: pre-commit cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
|
|
4
.github/workflows/macos-install.sh
vendored
|
@ -3,6 +3,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
|
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
|
||||||
|
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||||
|
|
||||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
PYTHONOPTIMIZE=0 python3 -m pip install cffi
|
||||||
python3 -m pip install coverage
|
python3 -m pip install coverage
|
||||||
|
@ -13,8 +14,7 @@ python3 -m pip install -U pytest-cov
|
||||||
python3 -m pip install -U pytest-timeout
|
python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
|
||||||
# TODO Remove condition when NumPy supports 3.12
|
python3 -m pip install numpy
|
||||||
if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi
|
|
||||||
|
|
||||||
# extra test images
|
# extra test images
|
||||||
pushd depends && ./install_extra_test_images.sh && popd
|
pushd depends && ./install_extra_test_images.sh && popd
|
||||||
|
|
14
.github/workflows/test-cygwin.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -36,7 +44,7 @@ jobs:
|
||||||
git config --global core.autocrlf input
|
git config --global core.autocrlf input
|
||||||
|
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Cygwin
|
- name: Install Cygwin
|
||||||
uses: cygwin/cygwin-install-action@v4
|
uses: cygwin/cygwin-install-action@v4
|
||||||
|
@ -102,10 +110,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
bash.exe .ci/install.sh
|
bash.exe .ci/install.sh
|
||||||
|
|
||||||
- name: Install latest NumPy
|
- name: Upgrade NumPy
|
||||||
shell: dash.exe -l "{0}"
|
shell: dash.exe -l "{0}"
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install -U numpy
|
python3 -m pip install -U "numpy<1.26"
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
shell: bash.exe -eo pipefail -o igncr "{0}"
|
shell: bash.exe -eo pipefail -o igncr "{0}"
|
||||||
|
|
10
.github/workflows/test-docker.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -59,7 +67,7 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
10
.github/workflows/test-mingw.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -34,7 +42,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up shell
|
- name: Set up shell
|
||||||
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
|
||||||
|
|
2
.github/workflows/test-valgrind.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
||||||
name: ${{ matrix.docker }}
|
name: ${{ matrix.docker }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build system information
|
- name: Build system information
|
||||||
run: python3 .github/workflows/system-info.py
|
run: python3 .github/workflows/system-info.py
|
||||||
|
|
16
.github/workflows/test-windows.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -24,7 +32,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
|
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
@ -32,16 +40,16 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Pillow
|
- name: Checkout Pillow
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Checkout cached dependencies
|
- name: Checkout cached dependencies
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: python-pillow/pillow-depends
|
repository: python-pillow/pillow-depends
|
||||||
path: winbuild\depends
|
path: winbuild\depends
|
||||||
|
|
||||||
- name: Checkout extra test images
|
- name: Checkout extra test images
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: python-pillow/test-images
|
repository: python-pillow/test-images
|
||||||
path: Tests\test-images
|
path: Tests\test-images
|
||||||
|
|
12
.github/workflows/test.yml
vendored
|
@ -4,11 +4,19 @@ on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- ".github/workflows/docs.yml"
|
- ".github/workflows/docs.yml"
|
||||||
|
- ".github/workflows/wheels*"
|
||||||
|
- ".gitmodules"
|
||||||
|
- ".travis.yml"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
|
- "wheels/**"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
@ -31,7 +39,7 @@ jobs:
|
||||||
python-version: [
|
python-version: [
|
||||||
"pypy3.10",
|
"pypy3.10",
|
||||||
"pypy3.9",
|
"pypy3.9",
|
||||||
"3.12-dev",
|
"3.12",
|
||||||
"3.11",
|
"3.11",
|
||||||
"3.10",
|
"3.10",
|
||||||
"3.9",
|
"3.9",
|
||||||
|
@ -48,7 +56,7 @@ jobs:
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
|
|
40
.github/workflows/wheels-build.sh
vendored
Executable file
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "multibuild"]
|
||||||
|
path = wheels/multibuild
|
||||||
|
url = https://github.com/multi-build/multibuild.git
|
|
@ -1,6 +1,12 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: 23.7.0
|
rev: v3.13.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [--py38-plus]
|
||||||
|
|
||||||
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
|
rev: 23.9.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--target-version=py38]
|
args: [--target-version=py38]
|
||||||
|
@ -23,7 +29,7 @@ repos:
|
||||||
- id: yesqa
|
- id: yesqa
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.5.3
|
rev: v1.5.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
@ -33,7 +39,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat]
|
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: v1.10.0
|
rev: v1.10.0
|
||||||
|
@ -44,23 +50,28 @@ repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
|
- id: check-executables-have-shebangs
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
exclude: ^Tests/images/
|
||||||
|
- id: trailing-whitespace
|
||||||
|
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||||
|
|
||||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||||
rev: v0.6.7
|
rev: v0.6.8
|
||||||
hooks:
|
hooks:
|
||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: 0.13.0
|
rev: 1.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
|
|
||||||
- repo: https://github.com/abravalheri/validate-pyproject
|
- repo: https://github.com/abravalheri/validate-pyproject
|
||||||
rev: v0.13
|
rev: v0.14
|
||||||
hooks:
|
hooks:
|
||||||
- id: validate-pyproject
|
- id: validate-pyproject
|
||||||
|
|
||||||
|
|
135
.travis.yml
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
if: tag IS present OR type = api
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- CONFIG_PATH=wheels/config.sh
|
||||||
|
- REPO_DIR=.
|
||||||
|
- PLAT=aarch64
|
||||||
|
- TEST_DEPENDS=pytest-timeout
|
||||||
|
|
||||||
|
language: python
|
||||||
|
# Default Python version is usually 3.6
|
||||||
|
python: "3.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
|
109
CHANGES.rst
|
@ -5,15 +5,78 @@ Changelog (Pillow)
|
||||||
10.1.0 (unreleased)
|
10.1.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Added ImageOps cover method #7412
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Catch struct.error from truncated EXIF when reading JPEG DPI #7458
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Consider default image when selecting mode for PNG save_all #7437
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added CMYK to RGB unpacker #7310
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improved flexibility of XMP parsing #7274
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Support reading 8-bit YCbCr TIFF images #7415
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Allow saving I;16B images as PNG #7302
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Corrected drawing I;16 points and writing I;16 text #7257
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Set blue channel to 128 for BC5S #7413
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Increase flexibility when reading IPTC fields #7319
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Set C palette to be empty by default #7289
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added gs_binary to control Ghostscript use on all platforms #7392
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Read bounding box information from the trailer of EPS files if specified #7382
|
||||||
|
[nopperl, radarhere]
|
||||||
|
|
||||||
|
- Added reading 8-bit color DDS images #7426
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added has_transparency_data #7420
|
||||||
|
[radarhere, hugovk]
|
||||||
|
|
||||||
|
- Fixed bug when reading BC5S DDS images #7401
|
||||||
|
[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
|
- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
- Expand JPEG buffer size when saving optimized or progressive #7345
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Added session type check for Linux in ImageGrab.grabclipboard() #7332
|
- Added session type check for Linux in ImageGrab.grabclipboard() #7332
|
||||||
[TheNooB2706, radarhere, hugovk]
|
[TheNooB2706, radarhere, hugovk]
|
||||||
|
|
||||||
- Read WebP duration after opening #7311
|
|
||||||
[k128, radarhere]
|
|
||||||
|
|
||||||
- Allow "loop=None" when saving GIF images #7329
|
- Allow "loop=None" when saving GIF images #7329
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
@ -38,6 +101,15 @@ Changelog (Pillow)
|
||||||
- Fix missing symbols when libtiff depends on libjpeg #7270
|
- Fix missing symbols when libtiff depends on libjpeg #7270
|
||||||
[heitbaum]
|
[heitbaum]
|
||||||
|
|
||||||
|
10.0.1 (2023-09-15)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Updated libwebp to 1.3.2 #7395
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Updated zlib to 1.3 #7344
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
10.0.0 (2023-07-01)
|
10.0.0 (2023-07-01)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
@ -5765,8 +5837,8 @@ http://svn.effbot.org/public/pil/
|
||||||
a polyline, independent of line angle.
|
a polyline, independent of line angle.
|
||||||
|
|
||||||
- Fixed bearing calculation and clipping in the ImageFont truetype
|
- Fixed bearing calculation and clipping in the ImageFont truetype
|
||||||
renderer; this could lead to clipped text, or crashes in the low-
|
renderer; this could lead to clipped text, or crashes in the low-level
|
||||||
level _imagingft module. (based on input from Adam Twardoch and
|
_imagingft module. (based on input from Adam Twardoch and
|
||||||
others).
|
others).
|
||||||
|
|
||||||
- Added ImageQt wrapper module, for converting PIL Image objects to
|
- Added ImageQt wrapper module, for converting PIL Image objects to
|
||||||
|
@ -5847,8 +5919,7 @@ http://svn.effbot.org/public/pil/
|
||||||
1.1.5c2 and 1.1.5 final
|
1.1.5c2 and 1.1.5 final
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
- Added experimental PERSPECTIVE transform method (from Jeff Breiden-
|
- Added experimental PERSPECTIVE transform method (from Jeff Breidenbach).
|
||||||
bach).
|
|
||||||
|
|
||||||
1.1.5c1
|
1.1.5c1
|
||||||
-------
|
-------
|
||||||
|
@ -5920,8 +5991,8 @@ http://svn.effbot.org/public/pil/
|
||||||
|
|
||||||
- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA".
|
- Fixed BILINEAR/BICUBIC/ANTIALIAS filtering for mode "LA".
|
||||||
|
|
||||||
- Added "getcolors()" method. This is similar to the existing histo-
|
- Added "getcolors()" method. This is similar to the existing histogram
|
||||||
gram method, but looks at color values instead of individual layers,
|
method, but looks at color values instead of individual layers,
|
||||||
and returns an unsorted list of (count, color) tuples.
|
and returns an unsorted list of (count, color) tuples.
|
||||||
|
|
||||||
By default, the method returns None if finds more than 256 colors.
|
By default, the method returns None if finds more than 256 colors.
|
||||||
|
@ -6137,8 +6208,8 @@ http://svn.effbot.org/public/pil/
|
||||||
|
|
||||||
- Added limited support for "bitfield compression" in BMP files
|
- Added limited support for "bitfield compression" in BMP files
|
||||||
and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This
|
and DIB buffers, for 15-bit, 16-bit, and 32-bit images. This
|
||||||
also fixes a problem with ImageGrab module when copying screen-
|
also fixes a problem with ImageGrab module when copying screendumps
|
||||||
dumps from the clipboard on 15/16/32-bit displays.
|
from the clipboard on 15/16/32-bit displays.
|
||||||
|
|
||||||
- Added experimental WAL (Quake 2 textures) loader. To use this
|
- Added experimental WAL (Quake 2 textures) loader. To use this
|
||||||
loader, import WalImageFile and call the "open" method in that
|
loader, import WalImageFile and call the "open" method in that
|
||||||
|
@ -6249,8 +6320,8 @@ http://svn.effbot.org/public/pil/
|
||||||
1.1.3 final
|
1.1.3 final
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
- Made setup.py look for old versions of zlib. For some back-
|
- Made setup.py look for old versions of zlib. For some background,
|
||||||
ground, see: https://zlib.net/advisory-2002-03-11.txt
|
see: https://zlib.net/advisory-2002-03-11.txt
|
||||||
|
|
||||||
1.1.3c2
|
1.1.3c2
|
||||||
-------
|
-------
|
||||||
|
@ -6441,8 +6512,8 @@ http://svn.effbot.org/public/pil/
|
||||||
supports all major PIL image modes (including F and I).
|
supports all major PIL image modes (including F and I).
|
||||||
|
|
||||||
- The ImageFile module now includes a Parser class, which can
|
- The ImageFile module now includes a Parser class, which can
|
||||||
be used to incrementally decode an image file (while down-
|
be used to incrementally decode an image file (while downloading
|
||||||
loading it from the net, for example). See the handbook for
|
it from the net, for example). See the handbook for
|
||||||
details.
|
details.
|
||||||
|
|
||||||
- "show" now converts non-standard modes to "L" or "RGB" (as
|
- "show" now converts non-standard modes to "L" or "RGB" (as
|
||||||
|
@ -6580,8 +6651,8 @@ http://svn.effbot.org/public/pil/
|
||||||
|
|
||||||
- The Image "transform" method now supports Image.QUAD transforms.
|
- The Image "transform" method now supports Image.QUAD transforms.
|
||||||
The data argument is an 8-tuple giving the upper left, lower
|
The data argument is an 8-tuple giving the upper left, lower
|
||||||
left, lower right, and upper right corner of the source quadri-
|
left, lower right, and upper right corner of the source quadrilateral.
|
||||||
lateral. Also added Image.MESH transform which takes a list
|
Also added Image.MESH transform which takes a list
|
||||||
of quadrilaterals.
|
of quadrilaterals.
|
||||||
|
|
||||||
- The Image "resize", "rotate", and "transform" methods now support
|
- The Image "resize", "rotate", and "transform" methods now support
|
||||||
|
@ -6806,8 +6877,8 @@ The test suite includes 400 individual tests.
|
||||||
neither "short", "int" nor "long" are 32-bit wide.
|
neither "short", "int" nor "long" are 32-bit wide.
|
||||||
|
|
||||||
- Added file= and data= keyword arguments to PhotoImage and BitmapImage.
|
- Added file= and data= keyword arguments to PhotoImage and BitmapImage.
|
||||||
This allows you to use them as drop-in replacements for the corre-
|
This allows you to use them as drop-in replacements for the corresponding
|
||||||
sponding Tkinter classes.
|
Tkinter classes.
|
||||||
|
|
||||||
- Removed bogus references to the crack coder (ImagingCrack).
|
- Removed bogus references to the crack coder (ImagingCrack).
|
||||||
|
|
||||||
|
|
|
@ -29,3 +29,4 @@ global-exclude .git*
|
||||||
global-exclude *.pyc
|
global-exclude *.pyc
|
||||||
global-exclude *.so
|
global-exclude *.so
|
||||||
prune .ci
|
prune .ci
|
||||||
|
prune wheels
|
||||||
|
|
14
README.md
|
@ -45,12 +45,12 @@ As of 2019, Pillow development is
|
||||||
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
|
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
|
||||||
alt="AppVeyor CI build status (Windows)"
|
alt="AppVeyor CI build status (Windows)"
|
||||||
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
|
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
|
||||||
<a href="https://github.com/python-pillow/pillow-wheels/actions"><img
|
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
|
||||||
alt="GitHub Actions wheels build status (Wheels)"
|
alt="GitHub Actions build status (Wheels)"
|
||||||
src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a>
|
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
|
||||||
<a href="https://app.travis-ci.com/github/python-pillow/pillow-wheels"><img
|
<a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
|
||||||
alt="Travis CI wheels build status (aarch64)"
|
alt="Travis CI wheels build status (aarch64)"
|
||||||
src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels"></a>
|
src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
|
||||||
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
|
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
|
||||||
alt="Code coverage"
|
alt="Code coverage"
|
||||||
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
|
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
|
||||||
|
@ -74,9 +74,9 @@ As of 2019, Pillow development is
|
||||||
<a href="https://pypi.org/project/Pillow/"><img
|
<a href="https://pypi.org/project/Pillow/"><img
|
||||||
alt="Number of PyPI downloads"
|
alt="Number of PyPI downloads"
|
||||||
src="https://img.shields.io/pypi/dm/pillow.svg"></a>
|
src="https://img.shields.io/pypi/dm/pillow.svg"></a>
|
||||||
<a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img
|
<a href="https://www.bestpractices.dev/projects/6331"><img
|
||||||
alt="OpenSSF Best Practices"
|
alt="OpenSSF Best Practices"
|
||||||
src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a>
|
src="https://www.bestpractices.dev/projects/6331/badge"></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
17
RELEASING.md
|
@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
||||||
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
||||||
* [ ] Develop and prepare release in `main` branch.
|
* [ ] Develop and prepare release in `main` branch.
|
||||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
||||||
* [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions.
|
* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them.
|
||||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||||
* [ ] Update `CHANGES.rst`.
|
* [ ] Update `CHANGES.rst`.
|
||||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||||
|
@ -99,17 +99,14 @@ Released as needed privately to individual vendors for critical security-related
|
||||||
## Binary Distributions
|
## Binary Distributions
|
||||||
|
|
||||||
### macOS and Linux
|
### macOS and Linux
|
||||||
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
|
* [ ] Download wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
|
||||||
|
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/python-pillow/pillow-wheels
|
gh run download --dir dist
|
||||||
cd pillow-wheels
|
# select dist-x.y.z
|
||||||
./update-pillow-tag.sh [[release tag]]
|
|
||||||
```
|
|
||||||
* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
|
|
||||||
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo:
|
|
||||||
```bash
|
|
||||||
gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels
|
|
||||||
```
|
```
|
||||||
|
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
|
||||||
|
and copy into `dist`.
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
|
||||||
|
|
0
Tests/check_j2k_leaks.py
Executable file → Normal file
|
@ -91,7 +91,7 @@ def assert_image_equal(a, b, msg=None):
|
||||||
if HAS_UPLOADER:
|
if HAS_UPLOADER:
|
||||||
try:
|
try:
|
||||||
url = test_image_results.upload(a, b)
|
url = test_image_results.upload(a, b)
|
||||||
logger.error(f"Url for test images: {url}")
|
logger.error("URL for test images: %s", url)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ def assert_image_similar(a, b, epsilon, msg=None):
|
||||||
if HAS_UPLOADER:
|
if HAS_UPLOADER:
|
||||||
try:
|
try:
|
||||||
url = test_image_results.upload(a, b)
|
url = test_image_results.upload(a, b)
|
||||||
logger.error(f"Url for test images: {url}")
|
logger.exception("URL for test images: %s", url)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise e
|
raise e
|
||||||
|
|
|
@ -22,4 +22,3 @@ and that the name of ICC shall not be used in advertising or publicity
|
||||||
pertaining to distribution of the software without specific, written
|
pertaining to distribution of the software without specific, written
|
||||||
prior permission. ICC makes no representations about the suitability
|
prior permission. ICC makes no representations about the suitability
|
||||||
of this software for any purpose.
|
of this software for any purpose.
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 95 KiB |
BIN
Tests/images/bc5u.dds
Normal file
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 180 B |
0
Tests/images/negative_size.ppm
Executable file → Normal file
BIN
Tests/images/palette.dds
Normal file
BIN
Tests/images/truncated_exif_dpi.jpg
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
Tests/images/xmp_no_prefix.jpg
Normal file
After Width: | Height: | Size: 788 B |
BIN
Tests/images/xmp_padded.jpg
Normal file
After Width: | Height: | Size: 778 B |
BIN
Tests/images/zero_bb_eof_before_boundingbox.eps
Normal file
BIN
Tests/images/zero_bb_trailer.eps
Normal file
|
@ -673,10 +673,16 @@ def test_seek_after_close():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
||||||
def test_different_modes_in_later_frames(mode, tmp_path):
|
@pytest.mark.parametrize("default_image", (True, False))
|
||||||
|
def test_different_modes_in_later_frames(mode, default_image, tmp_path):
|
||||||
test_file = str(tmp_path / "temp.png")
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
im = Image.new("L", (1, 1))
|
im = Image.new("L", (1, 1))
|
||||||
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
|
im.save(
|
||||||
|
test_file,
|
||||||
|
save_all=True,
|
||||||
|
default_image=default_image,
|
||||||
|
append_images=[Image.new(mode, (1, 1))],
|
||||||
|
)
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
assert reloaded.mode == mode
|
assert reloaded.mode == mode
|
||||||
|
|
|
@ -16,6 +16,7 @@ TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
|
||||||
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
|
||||||
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
|
||||||
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
|
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
|
||||||
|
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
|
||||||
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
|
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
|
||||||
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
|
TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
|
||||||
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
|
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
|
||||||
|
@ -81,10 +82,18 @@ def test_sanity_ati1():
|
||||||
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
|
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
|
||||||
|
|
||||||
|
|
||||||
def test_sanity_ati2():
|
@pytest.mark.parametrize(
|
||||||
"""Check ATI2 images can be opened"""
|
"image_path",
|
||||||
|
(
|
||||||
|
TEST_FILE_ATI2,
|
||||||
|
# hexeditted to use BC5U FourCC
|
||||||
|
TEST_FILE_BC5U,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_sanity_ati2_bc5u(image_path):
|
||||||
|
"""Check ATI2 and BC5U images can be opened"""
|
||||||
|
|
||||||
with Image.open(TEST_FILE_ATI2) as im:
|
with Image.open(image_path) as im:
|
||||||
im.load()
|
im.load()
|
||||||
|
|
||||||
assert im.format == "DDS"
|
assert im.format == "DDS"
|
||||||
|
@ -289,6 +298,11 @@ def test_dxt5_colorblock_alpha_issue_4142():
|
||||||
assert px[2] != 0
|
assert px[2] != 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_palette():
|
||||||
|
with Image.open("Tests/images/palette.dds") as im:
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/transparent.gif")
|
||||||
|
|
||||||
|
|
||||||
def test_unimplemented_pixel_format():
|
def test_unimplemented_pixel_format():
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
|
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
|
||||||
|
|
|
@ -8,6 +8,7 @@ from .helper import (
|
||||||
assert_image_similar,
|
assert_image_similar,
|
||||||
assert_image_similar_tofile,
|
assert_image_similar_tofile,
|
||||||
hopper,
|
hopper,
|
||||||
|
is_win32,
|
||||||
mark_if_feature_version,
|
mark_if_feature_version,
|
||||||
skip_unless_feature,
|
skip_unless_feature,
|
||||||
)
|
)
|
||||||
|
@ -98,6 +99,20 @@ def test_load():
|
||||||
assert im.load()[0, 0] == (255, 255, 255)
|
assert im.load()[0, 0] == (255, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def test_binary():
|
||||||
|
if HAS_GHOSTSCRIPT:
|
||||||
|
assert EpsImagePlugin.gs_binary is not None
|
||||||
|
else:
|
||||||
|
assert EpsImagePlugin.gs_binary is False
|
||||||
|
|
||||||
|
if not is_win32():
|
||||||
|
assert EpsImagePlugin.gs_windows_binary is None
|
||||||
|
elif not HAS_GHOSTSCRIPT:
|
||||||
|
assert EpsImagePlugin.gs_windows_binary is False
|
||||||
|
else:
|
||||||
|
assert EpsImagePlugin.gs_windows_binary is not None
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
invalid_file = "Tests/images/flower.jpg"
|
invalid_file = "Tests/images/flower.jpg"
|
||||||
with pytest.raises(SyntaxError):
|
with pytest.raises(SyntaxError):
|
||||||
|
@ -404,3 +419,18 @@ def test_timeout(test_file):
|
||||||
with pytest.raises(Image.UnidentifiedImageError):
|
with pytest.raises(Image.UnidentifiedImageError):
|
||||||
with Image.open(f):
|
with Image.open(f):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_bounding_box_in_trailer():
|
||||||
|
# Check bounding boxes are parsed in the same way
|
||||||
|
# when specified in the header and the trailer
|
||||||
|
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
|
||||||
|
FILE1
|
||||||
|
) as header_image:
|
||||||
|
assert trailer_image.size == header_image.size
|
||||||
|
|
||||||
|
|
||||||
|
def test_eof_before_bounding_box():
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
|
||||||
|
pass
|
||||||
|
|
|
@ -205,14 +205,14 @@ def test_optimize_full_l():
|
||||||
|
|
||||||
|
|
||||||
def test_optimize_if_palette_can_be_reduced_by_half():
|
def test_optimize_if_palette_can_be_reduced_by_half():
|
||||||
with Image.open("Tests/images/test.colors.gif") as im:
|
im = Image.new("P", (8, 1))
|
||||||
# Reduce dimensions because original is too big for _get_optimize()
|
im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
|
||||||
im = im.resize((591, 443))
|
for i in range(8):
|
||||||
im_rgb = im.convert("RGB")
|
im.putpixel((i, 0), (i + 1, 0, 0))
|
||||||
|
|
||||||
for optimize, colors in ((False, 256), (True, 8)):
|
for optimize, colors in ((False, 256), (True, 8)):
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
im_rgb.save(out, "GIF", optimize=optimize)
|
im.save(out, "GIF", optimize=optimize)
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
assert len(reloaded.palette.palette) // 3 == colors
|
assert len(reloaded.palette.palette) // 3 == colors
|
||||||
|
|
||||||
|
@ -1180,18 +1180,17 @@ def test_palette_save_L(tmp_path):
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_P(tmp_path):
|
def test_palette_save_P(tmp_path):
|
||||||
# Pass in a different palette, then construct what the image would look like.
|
im = Image.new("P", (1, 2))
|
||||||
# Forcing a non-straight grayscale palette.
|
im.putpixel((0, 1), 1)
|
||||||
|
|
||||||
im = hopper("P")
|
|
||||||
palette = bytes(255 - i // 3 for i in range(768))
|
|
||||||
|
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im.save(out, palette=palette)
|
im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
|
||||||
|
|
||||||
with Image.open(out) as reloaded:
|
with Image.open(out) as reloaded:
|
||||||
im.putpalette(palette)
|
reloaded_rgb = reloaded.convert("RGB")
|
||||||
assert_image_equal(reloaded, im)
|
|
||||||
|
assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
|
||||||
|
assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
|
||||||
|
|
||||||
|
|
||||||
def test_palette_save_duplicate_entries(tmp_path):
|
def test_palette_save_duplicate_entries(tmp_path):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
from io import StringIO
|
from io import BytesIO, StringIO
|
||||||
|
|
||||||
from PIL import Image, IptcImagePlugin
|
from PIL import Image, IptcImagePlugin
|
||||||
|
|
||||||
|
@ -30,6 +30,36 @@ def test_getiptcinfo_jpg_found():
|
||||||
assert iptc[(2, 101)] == b"Hungary"
|
assert iptc[(2, 101)] == b"Hungary"
|
||||||
|
|
||||||
|
|
||||||
|
def test_getiptcinfo_fotostation():
|
||||||
|
# Arrange
|
||||||
|
with open(TEST_FILE, "rb") as fp:
|
||||||
|
data = bytearray(fp.read())
|
||||||
|
data[86] = 240
|
||||||
|
f = BytesIO(data)
|
||||||
|
with Image.open(f) as im:
|
||||||
|
# Act
|
||||||
|
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
for tag in iptc.keys():
|
||||||
|
if tag[0] == 240:
|
||||||
|
return
|
||||||
|
assert False, "FotoStation tag not found"
|
||||||
|
|
||||||
|
|
||||||
|
def test_getiptcinfo_zero_padding():
|
||||||
|
# Arrange
|
||||||
|
with Image.open(TEST_FILE) as im:
|
||||||
|
im.info["photoshop"][0x0404] += b"\x00\x00\x00"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
iptc = IptcImagePlugin.getiptcinfo(im)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert isinstance(iptc, dict)
|
||||||
|
assert len(iptc) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_getiptcinfo_tiff_none():
|
def test_getiptcinfo_tiff_none():
|
||||||
# Arrange
|
# Arrange
|
||||||
with Image.open("Tests/images/hopper.tif") as im:
|
with Image.open("Tests/images/hopper.tif") as im:
|
||||||
|
|
|
@ -767,6 +767,13 @@ class TestFileJpeg:
|
||||||
# This should return the default
|
# This should return the default
|
||||||
assert im.info.get("dpi") == (72, 72)
|
assert im.info.get("dpi") == (72, 72)
|
||||||
|
|
||||||
|
def test_dpi_exif_truncated(self):
|
||||||
|
# Arrange
|
||||||
|
with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
|
||||||
|
# Act / Assert
|
||||||
|
# This should return the default
|
||||||
|
assert im.info.get("dpi") == (72, 72)
|
||||||
|
|
||||||
def test_no_dpi_in_exif(self):
|
def test_no_dpi_in_exif(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
|
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
|
||||||
|
@ -882,7 +889,10 @@ class TestFileJpeg:
|
||||||
def test_getxmp(self):
|
def test_getxmp(self):
|
||||||
with Image.open("Tests/images/xmp_test.jpg") as im:
|
with Image.open("Tests/images/xmp_test.jpg") as im:
|
||||||
if ElementTree is None:
|
if ElementTree is None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
@ -905,6 +915,28 @@ class TestFileJpeg:
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
|
|
||||||
|
def test_getxmp_no_prefix(self):
|
||||||
|
with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
|
||||||
|
if ElementTree is None:
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
|
assert im.getxmp() == {}
|
||||||
|
else:
|
||||||
|
assert im.getxmp() == {"xmpmeta": {"key": "value"}}
|
||||||
|
|
||||||
|
def test_getxmp_padded(self):
|
||||||
|
with Image.open("Tests/images/xmp_padded.jpg") as im:
|
||||||
|
if ElementTree is None:
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
|
assert im.getxmp() == {}
|
||||||
|
else:
|
||||||
|
assert im.getxmp() == {"xmpmeta": None}
|
||||||
|
|
||||||
@pytest.mark.timeout(timeout=1)
|
@pytest.mark.timeout(timeout=1)
|
||||||
def test_eof(self):
|
def test_eof(self):
|
||||||
# Even though this decoder never says that it is finished
|
# Even though this decoder never says that it is finished
|
||||||
|
|
|
@ -8,7 +8,7 @@ from collections import namedtuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features
|
from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features
|
||||||
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
|
||||||
|
|
||||||
from .helper import (
|
from .helper import (
|
||||||
|
@ -1035,7 +1035,18 @@ class TestFileLibTiff(LibTiffTestCase):
|
||||||
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||||
for i in range(2, 9):
|
for i in range(2, 9):
|
||||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||||
|
assert 274 in im.tag_v2
|
||||||
|
|
||||||
im.load()
|
im.load()
|
||||||
|
assert 274 not in im.tag_v2
|
||||||
|
|
||||||
|
assert_image_similar(base_im, im, 0.7)
|
||||||
|
|
||||||
|
def test_exif_transpose(self):
|
||||||
|
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||||
|
for i in range(2, 9):
|
||||||
|
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||||
|
im = ImageOps.exif_transpose(im)
|
||||||
|
|
||||||
assert_image_similar(base_im, im, 0.7)
|
assert_image_similar(base_im, im, 0.7)
|
||||||
|
|
||||||
|
|
|
@ -92,11 +92,11 @@ class TestFilePng:
|
||||||
assert im.format == "PNG"
|
assert im.format == "PNG"
|
||||||
assert im.get_format_mimetype() == "image/png"
|
assert im.get_format_mimetype() == "image/png"
|
||||||
|
|
||||||
for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
|
for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
|
||||||
im = hopper(mode)
|
im = hopper(mode)
|
||||||
im.save(test_file)
|
im.save(test_file)
|
||||||
with Image.open(test_file) as reloaded:
|
with Image.open(test_file) as reloaded:
|
||||||
if mode == "I;16":
|
if mode in ("I;16", "I;16B"):
|
||||||
reloaded = reloaded.convert(mode)
|
reloaded = reloaded.convert(mode)
|
||||||
assert_image_equal(reloaded, im)
|
assert_image_equal(reloaded, im)
|
||||||
|
|
||||||
|
@ -665,7 +665,10 @@ class TestFilePng:
|
||||||
def test_getxmp(self):
|
def test_getxmp(self):
|
||||||
with Image.open("Tests/images/color_snakes.png") as im:
|
with Image.open("Tests/images/color_snakes.png") as im:
|
||||||
if ElementTree is None:
|
if ElementTree is None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
|
@ -2,7 +2,7 @@ import pytest
|
||||||
|
|
||||||
from PIL import Image, QoiImagePlugin
|
from PIL import Image, QoiImagePlugin
|
||||||
|
|
||||||
from .helper import assert_image_equal_tofile, assert_image_similar_tofile
|
from .helper import assert_image_equal_tofile
|
||||||
|
|
||||||
|
|
||||||
def test_sanity():
|
def test_sanity():
|
||||||
|
@ -18,7 +18,7 @@ def test_sanity():
|
||||||
assert im.size == (162, 150)
|
assert im.size == (162, 150)
|
||||||
assert im.format == "QOI"
|
assert im.format == "QOI"
|
||||||
|
|
||||||
assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03)
|
assert_image_equal_tofile(im, "Tests/images/pil123rgba.png")
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_file():
|
def test_invalid_file():
|
||||||
|
|
|
@ -734,7 +734,10 @@ class TestFileTiff:
|
||||||
def test_getxmp(self):
|
def test_getxmp(self):
|
||||||
with Image.open("Tests/images/lab.tif") as im:
|
with Image.open("Tests/images/lab.tif") as im:
|
||||||
if ElementTree is None:
|
if ElementTree is None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
xmp = im.getxmp()
|
xmp = im.getxmp()
|
||||||
|
|
|
@ -233,4 +233,15 @@ class TestFileWebp:
|
||||||
im.save(out_webp, save_all=True)
|
im.save(out_webp, save_all=True)
|
||||||
|
|
||||||
with Image.open(out_webp) as reloaded:
|
with Image.open(out_webp) as reloaded:
|
||||||
|
reloaded.load()
|
||||||
assert reloaded.info["duration"] == 1000
|
assert reloaded.info["duration"] == 1000
|
||||||
|
|
||||||
|
def test_roundtrip_rgba_palette(self, tmp_path):
|
||||||
|
temp_file = str(tmp_path / "temp.webp")
|
||||||
|
im = Image.new("RGBA", (1, 1)).convert("P")
|
||||||
|
assert im.mode == "P"
|
||||||
|
assert im.palette.mode == "RGBA"
|
||||||
|
im.save(temp_file)
|
||||||
|
|
||||||
|
with Image.open(temp_file) as im:
|
||||||
|
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||||
|
|
|
@ -118,7 +118,10 @@ def test_getxmp():
|
||||||
|
|
||||||
with Image.open("Tests/images/flower2.webp") as im:
|
with Image.open("Tests/images/flower2.webp") as im:
|
||||||
if ElementTree is None:
|
if ElementTree is None:
|
||||||
with pytest.warns(UserWarning):
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match="XMP data cannot be read without defusedxml dependency",
|
||||||
|
):
|
||||||
assert im.getxmp() == {}
|
assert im.getxmp() == {}
|
||||||
else:
|
else:
|
||||||
assert (
|
assert (
|
||||||
|
|
|
@ -638,8 +638,8 @@ class TestImage:
|
||||||
im.remap_palette(None)
|
im.remap_palette(None)
|
||||||
|
|
||||||
def test_remap_palette_transparency(self):
|
def test_remap_palette_transparency(self):
|
||||||
im = Image.new("P", (1, 2))
|
im = Image.new("P", (1, 2), (0, 0, 0))
|
||||||
im.putpixel((0, 1), 1)
|
im.putpixel((0, 1), (255, 0, 0))
|
||||||
im.info["transparency"] = 0
|
im.info["transparency"] = 0
|
||||||
|
|
||||||
im_remapped = im.remap_palette([1, 0])
|
im_remapped = im.remap_palette([1, 0])
|
||||||
|
@ -906,6 +906,31 @@ class TestImage:
|
||||||
im = Image.new("RGB", size)
|
im = Image.new("RGB", size)
|
||||||
assert im.tobytes() == b""
|
assert im.tobytes() == b""
|
||||||
|
|
||||||
|
def test_has_transparency_data(self):
|
||||||
|
for mode in ("1", "L", "P", "RGB"):
|
||||||
|
im = Image.new(mode, (1, 1))
|
||||||
|
assert not im.has_transparency_data
|
||||||
|
|
||||||
|
for mode in ("LA", "La", "PA", "RGBA", "RGBa"):
|
||||||
|
im = Image.new(mode, (1, 1))
|
||||||
|
assert im.has_transparency_data
|
||||||
|
|
||||||
|
# P mode with "transparency" info
|
||||||
|
with Image.open("Tests/images/first_frame_transparency.gif") as im:
|
||||||
|
assert "transparency" in im.info
|
||||||
|
assert im.has_transparency_data
|
||||||
|
|
||||||
|
# RGB mode with "transparency" info
|
||||||
|
with Image.open("Tests/images/rgb_trns.png") as im:
|
||||||
|
assert "transparency" in im.info
|
||||||
|
assert im.has_transparency_data
|
||||||
|
|
||||||
|
# P mode with RGBA palette
|
||||||
|
im = Image.new("RGBA", (1, 1)).convert("P")
|
||||||
|
assert im.mode == "P"
|
||||||
|
assert im.palette.mode == "RGBA"
|
||||||
|
assert im.has_transparency_data
|
||||||
|
|
||||||
def test_apply_transparency(self):
|
def test_apply_transparency(self):
|
||||||
im = Image.new("P", (1, 1))
|
im = Image.new("P", (1, 1))
|
||||||
im.putpalette((0, 0, 0, 1, 1, 1))
|
im.putpalette((0, 0, 0, 1, 1, 1))
|
||||||
|
|
|
@ -130,9 +130,16 @@ class TestImageGetPixel(AccessTest):
|
||||||
bands = Image.getmodebands(mode)
|
bands = Image.getmodebands(mode)
|
||||||
if bands == 1:
|
if bands == 1:
|
||||||
return 1
|
return 1
|
||||||
|
if mode in ("BGR;15", "BGR;16"):
|
||||||
|
# These modes have less than 8 bits per band
|
||||||
|
# So (1, 2, 3) cannot be roundtripped
|
||||||
|
return (16, 32, 49)
|
||||||
return tuple(range(1, bands + 1))
|
return tuple(range(1, bands + 1))
|
||||||
|
|
||||||
def check(self, mode, expected_color=None):
|
def check(self, mode, expected_color=None):
|
||||||
|
if self._need_cffi_access and mode.startswith("BGR;"):
|
||||||
|
pytest.skip("Support not added to deprecated module for BGR;* modes")
|
||||||
|
|
||||||
if not expected_color:
|
if not expected_color:
|
||||||
expected_color = self.color(mode)
|
expected_color = self.color(mode)
|
||||||
|
|
||||||
|
@ -203,6 +210,9 @@ class TestImageGetPixel(AccessTest):
|
||||||
"F",
|
"F",
|
||||||
"P",
|
"P",
|
||||||
"PA",
|
"PA",
|
||||||
|
"BGR;15",
|
||||||
|
"BGR;16",
|
||||||
|
"BGR;24",
|
||||||
"RGB",
|
"RGB",
|
||||||
"RGBA",
|
"RGBA",
|
||||||
"RGBX",
|
"RGBX",
|
||||||
|
@ -213,6 +223,10 @@ class TestImageGetPixel(AccessTest):
|
||||||
def test_basic(self, mode):
|
def test_basic(self, mode):
|
||||||
self.check(mode)
|
self.check(mode)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
im = hopper()
|
||||||
|
assert im.getpixel([0, 0]) == (20, 20, 70)
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
|
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
|
||||||
|
|
|
@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
|
||||||
f = str(tmp_path / "temp.png")
|
f = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
im_l = im.convert("L")
|
im_l = im.convert("L")
|
||||||
assert im_l.info["transparency"] == 1 # undone
|
assert im_l.info["transparency"] == 0
|
||||||
im_l.save(f)
|
im_l.save(f)
|
||||||
|
|
||||||
im_rgb = im.convert("RGB")
|
im_rgb = im.convert("RGB")
|
||||||
assert im_rgb.info["transparency"] == (0, 1, 2) # undone
|
assert im_rgb.info["transparency"] == (0, 0, 0)
|
||||||
im_rgb.save(f)
|
im_rgb.save(f)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,15 @@ def test_mode_F():
|
||||||
assert list(im.getdata()) == target
|
assert list(im.getdata()) == target
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
|
||||||
|
def test_mode_BGR(mode):
|
||||||
|
data = [(16, 32, 49), (32, 32, 98)]
|
||||||
|
im = Image.new(mode, (1, 2))
|
||||||
|
im.putdata(data)
|
||||||
|
|
||||||
|
assert list(im.getdata()) == data
|
||||||
|
|
||||||
|
|
||||||
def test_array_B():
|
def test_array_B():
|
||||||
# shouldn't segfault
|
# shouldn't segfault
|
||||||
# see https://github.com/python-pillow/Pillow/issues/1008
|
# see https://github.com/python-pillow/Pillow/issues/1008
|
||||||
|
|
|
@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
|
||||||
im.putpalette(palette, mode)
|
im.putpalette(palette, mode)
|
||||||
assert im.getpalette() == [1, 2, 3]
|
assert im.getpalette() == [1, 2, 3]
|
||||||
assert im.palette.colors == {(1, 2, 3, 4): 0}
|
assert im.palette.colors == {(1, 2, 3, 4): 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_palette():
|
||||||
|
im = Image.new("P", (1, 1))
|
||||||
|
assert im.getpalette() == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_undefined_palette_index():
|
||||||
|
im = Image.new("P", (1, 1), 3)
|
||||||
|
im.putpalette((1, 2, 3))
|
||||||
|
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)
|
||||||
|
|
|
@ -587,6 +587,18 @@ def test_point(points):
|
||||||
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
|
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_point_I16():
|
||||||
|
# Arrange
|
||||||
|
im = Image.new("I;16", (1, 1))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
draw.point((0, 0), fill=0x1234)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert im.getpixel((0, 0)) == 0x1234
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("points", POINTS)
|
@pytest.mark.parametrize("points", POINTS)
|
||||||
def test_polygon(points):
|
def test_polygon(points):
|
||||||
# Arrange
|
# Arrange
|
||||||
|
@ -733,7 +745,7 @@ def test_rectangle_I16(bbox):
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
draw.rectangle(bbox, fill="black", outline="green")
|
draw.rectangle(bbox, outline=0xFFFF)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
|
assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
|
||||||
|
|
|
@ -141,7 +141,9 @@ def test_I16(font):
|
||||||
draw = ImageDraw.Draw(im)
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
draw.text((10, 10), txt, font=font)
|
draw.text((10, 10), txt, fill=0xFFFE, font=font)
|
||||||
|
|
||||||
|
assert im.getpixel((12, 14)) == 0xFFFE
|
||||||
|
|
||||||
target = "Tests/images/transparent_background_text_L.png"
|
target = "Tests/images/transparent_background_text_L.png"
|
||||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||||
|
|
|
@ -39,6 +39,9 @@ def test_sanity():
|
||||||
ImageOps.contain(hopper("L"), (128, 128))
|
ImageOps.contain(hopper("L"), (128, 128))
|
||||||
ImageOps.contain(hopper("RGB"), (128, 128))
|
ImageOps.contain(hopper("RGB"), (128, 128))
|
||||||
|
|
||||||
|
ImageOps.cover(hopper("L"), (128, 128))
|
||||||
|
ImageOps.cover(hopper("RGB"), (128, 128))
|
||||||
|
|
||||||
ImageOps.crop(hopper("L"), 1)
|
ImageOps.crop(hopper("L"), 1)
|
||||||
ImageOps.crop(hopper("RGB"), 1)
|
ImageOps.crop(hopper("RGB"), 1)
|
||||||
|
|
||||||
|
@ -119,6 +122,20 @@ def test_contain_round():
|
||||||
assert new_im.height == 5
|
assert new_im.height == 5
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"image_name, expected_size",
|
||||||
|
(
|
||||||
|
("colr_bungee.png", (1024, 256)), # landscape
|
||||||
|
("imagedraw_stroke_multiline.png", (256, 640)), # portrait
|
||||||
|
("hopper.png", (256, 256)), # square
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_cover(image_name, expected_size):
|
||||||
|
with Image.open("Tests/images/" + image_name) as im:
|
||||||
|
new_im = ImageOps.cover(im, (256, 256))
|
||||||
|
assert new_im.size == expected_size
|
||||||
|
|
||||||
|
|
||||||
def test_pad():
|
def test_pad():
|
||||||
# Same ratio
|
# Same ratio
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
|
@ -340,6 +340,17 @@ class TestLibUnpack:
|
||||||
self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
|
self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
|
||||||
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
|
self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
|
||||||
|
|
||||||
|
self.assert_unpack(
|
||||||
|
"RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_BGR(self):
|
||||||
|
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
|
||||||
|
self.assert_unpack(
|
||||||
|
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
|
||||||
|
)
|
||||||
|
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
|
||||||
|
|
||||||
def test_RGBA(self):
|
def test_RGBA(self):
|
||||||
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
|
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
|
||||||
self.assert_unpack(
|
self.assert_unpack(
|
||||||
|
|
0
_custom_build/backend.py
Executable file → Normal file
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install libimagequant
|
# install libimagequant
|
||||||
|
|
||||||
archive=libimagequant-4.2.0
|
archive=libimagequant-4.2.1
|
||||||
|
|
||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -11,4 +11,3 @@ pushd $archive
|
||||||
meson build --prefix=/usr && sudo ninja -C build install
|
meson build --prefix=/usr && sudo ninja -C build install
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|
|
@ -15,4 +15,3 @@ make && sudo make install
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# install webp
|
# install webp
|
||||||
|
|
||||||
archive=libwebp-1.3.1
|
archive=libwebp-1.3.2
|
||||||
|
|
||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,3 @@
|
||||||
|
|
||||||
pkg install -y python ndk-sysroot clang make \
|
pkg install -y python ndk-sysroot clang make \
|
||||||
libjpeg-turbo
|
libjpeg-turbo
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ The fork author's goal is to foster and support active development of PIL throug
|
||||||
|
|
||||||
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
|
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
|
||||||
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
|
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
|
||||||
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels
|
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow
|
||||||
.. _GitHub: https://github.com/python-pillow/Pillow
|
.. _GitHub: https://github.com/python-pillow/Pillow
|
||||||
.. _Python Package Index: https://pypi.org/project/Pillow/
|
.. _Python Package Index: https://pypi.org/project/Pillow/
|
||||||
|
|
||||||
|
|
|
@ -318,7 +318,7 @@ def setup(app):
|
||||||
|
|
||||||
|
|
||||||
linkcheck_allowed_redirects = {
|
linkcheck_allowed_redirects = {
|
||||||
r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501
|
r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*", # noqa: E501
|
||||||
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
|
r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501
|
||||||
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
|
r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501
|
||||||
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501
|
r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501
|
||||||
|
|
BIN
docs/example/image_thumbnail.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_contain.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
docs/example/imageops_cover.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
docs/example/imageops_fit.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/example/imageops_pad.png
Normal file
After Width: | Height: | Size: 19 KiB |
|
@ -63,8 +63,35 @@ DDS
|
||||||
^^^
|
^^^
|
||||||
|
|
||||||
DDS is a popular container texture format used in video games and natively supported
|
DDS is a popular container texture format used in video games and natively supported
|
||||||
by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1,
|
by DirectX.
|
||||||
DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode.
|
|
||||||
|
DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode.
|
||||||
|
|
||||||
|
.. versionadded:: 3.4.0
|
||||||
|
DXT3 images can be read in ``RGB`` mode and DX10 images can be read in
|
||||||
|
``RGB`` and ``RGBA`` mode.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0.0
|
||||||
|
Uncompressed ``RGBA`` images can be read.
|
||||||
|
|
||||||
|
|
||||||
|
.. versionadded:: 8.3.0
|
||||||
|
BC5S images can be opened in ``RGB`` mode, and uncompressed ``RGB`` images
|
||||||
|
can be read. Uncompressed data can also be saved to image files.
|
||||||
|
|
||||||
|
|
||||||
|
.. versionadded:: 9.3.0
|
||||||
|
ATI1 images can be opened in ``L`` mode and ATI2 images can be opened in
|
||||||
|
``RGB`` mode.
|
||||||
|
|
||||||
|
.. versionadded:: 9.4.0
|
||||||
|
Uncompressed ``L`` ("luminance") and ``LA`` images can be opened and saved.
|
||||||
|
|
||||||
|
|
||||||
|
.. versionadded:: 10.1.0
|
||||||
|
BC5U can be read in ``RGB`` mode, and 8-bit color indexed images can be read
|
||||||
|
in ``P`` mode.
|
||||||
|
|
||||||
|
|
||||||
DIB
|
DIB
|
||||||
^^^
|
^^^
|
||||||
|
@ -88,8 +115,13 @@ in ``L``, ``RGB`` and ``CMYK`` modes.
|
||||||
Loading
|
Loading
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it
|
||||||
|
also searches for "gswin32c" and "gswin64c". To customise this behaviour,
|
||||||
|
``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to
|
||||||
|
use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use.
|
||||||
|
|
||||||
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
|
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
|
||||||
method with the following parameters to affect how Ghostscript renders the EPS
|
method with the following parameters to affect how Ghostscript renders the EPS.
|
||||||
|
|
||||||
**scale**
|
**scale**
|
||||||
Affects the scale of the resultant rasterized image. If the EPS suggests
|
Affects the scale of the resultant rasterized image. If the EPS suggests
|
||||||
|
|
|
@ -268,6 +268,37 @@ true, to provide for the same changes to the image's size.
|
||||||
A more general form of image transformations can be carried out via the
|
A more general form of image transformations can be carried out via the
|
||||||
:py:meth:`~PIL.Image.Image.transform` method.
|
:py:meth:`~PIL.Image.Image.transform` method.
|
||||||
|
|
||||||
|
Relative resizing
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Instead of calculating the size of the new image when resizing, you can also
|
||||||
|
choose to resize relative to a given size.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
size = (100, 150)
|
||||||
|
with Image.open("Tests/images/hopper.png") as im:
|
||||||
|
ImageOps.contain(im, size).save("imageops_contain.png")
|
||||||
|
ImageOps.cover(im, size).save("imageops_cover.png")
|
||||||
|
ImageOps.fit(im, size).save("imageops_fit.png")
|
||||||
|
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
||||||
|
|
||||||
|
# thumbnail() can also be used,
|
||||||
|
# but will modify the image object in place
|
||||||
|
im.thumbnail(size)
|
||||||
|
im.save("imageops_thumbnail.png")
|
||||||
|
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||||
|
+================+===========================================+============================================+==========================================+========================================+========================================+
|
||||||
|
|Given size | ``(150, 100)`` | ``(150, 100)`` | ``(150, 150)`` | ``(150, 100)`` | ``(150, 100)`` |
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``150×100`` | ``150×100`` |
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
|
||||||
.. _color-transforms:
|
.. _color-transforms:
|
||||||
|
|
||||||
Color transforms
|
Color transforms
|
||||||
|
|
|
@ -37,12 +37,12 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
|
||||||
:target: https://ci.appveyor.com/project/python-pillow/Pillow
|
:target: https://ci.appveyor.com/project/python-pillow/Pillow
|
||||||
:alt: AppVeyor CI build status (Windows)
|
:alt: AppVeyor CI build status (Windows)
|
||||||
|
|
||||||
.. image:: https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg
|
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
|
||||||
:target: https://github.com/python-pillow/pillow-wheels/actions
|
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
|
||||||
:alt: GitHub Actions wheels build status (Wheels)
|
:alt: GitHub Actions build status (Wheels)
|
||||||
|
|
||||||
.. image:: https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels
|
.. image:: https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels
|
||||||
:target: https://app.travis-ci.com/github/python-pillow/pillow-wheels
|
:target: https://app.travis-ci.com/github/python-pillow/Pillow
|
||||||
:alt: Travis CI wheels build status (aarch64)
|
:alt: Travis CI wheels build status (aarch64)
|
||||||
|
|
||||||
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
|
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
|
||||||
|
@ -69,8 +69,8 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
|
||||||
:target: https://pypi.org/project/Pillow/
|
:target: https://pypi.org/project/Pillow/
|
||||||
:alt: Number of PyPI downloads
|
:alt: Number of PyPI downloads
|
||||||
|
|
||||||
.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge
|
.. image:: https://www.bestpractices.dev/projects/6331/badge
|
||||||
:target: https://bestpractices.coreinfrastructure.org/projects/6331
|
:target: https://www.bestpractices.dev/projects/6331
|
||||||
:alt: OpenSSF Best Practices
|
:alt: OpenSSF Best Practices
|
||||||
|
|
||||||
.. image:: https://badges.gitter.im/python-pillow/Pillow.svg
|
.. image:: https://badges.gitter.im/python-pillow/Pillow.svg
|
||||||
|
|
|
@ -82,6 +82,8 @@ Install Pillow with :command:`pip`::
|
||||||
|
|
||||||
.. tab:: Windows
|
.. tab:: Windows
|
||||||
|
|
||||||
|
.. warning:: Pillow > 9.5.0 no longer includes 32-bit wheels.
|
||||||
|
|
||||||
We provide Pillow binaries for Windows compiled for the matrix of
|
We provide Pillow binaries for Windows compiled for the matrix of
|
||||||
supported Pythons in 64-bit versions in the wheel format. These binaries include
|
supported Pythons in 64-bit versions in the wheel format. These binaries include
|
||||||
support for all optional libraries except libimagequant and libxcb. Raqm support
|
support for all optional libraries except libimagequant and libxcb. Raqm support
|
||||||
|
@ -154,7 +156,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libtiff** provides compressed TIFF functionality
|
* **libtiff** provides compressed TIFF functionality
|
||||||
|
|
||||||
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1**
|
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
|
||||||
|
|
||||||
* **libfreetype** provides type related services
|
* **libfreetype** provides type related services
|
||||||
|
|
||||||
|
@ -180,7 +182,7 @@ Many of Pillow's features require external libraries:
|
||||||
|
|
||||||
* **libimagequant** provides improved color quantization
|
* **libimagequant** provides improved color quantization
|
||||||
|
|
||||||
* Pillow has been tested with libimagequant **2.6-4.2**
|
* Pillow has been tested with libimagequant **2.6-4.2.1**
|
||||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||||
the Pillow license, therefore we will not be distributing binaries
|
the Pillow license, therefore we will not be distributing binaries
|
||||||
with libimagequant support enabled.
|
with libimagequant support enabled.
|
||||||
|
@ -498,11 +500,13 @@ These platforms have been reported to work at the versions mentioned.
|
||||||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||||
| | | versions | | Pillow version | | processors |
|
| | | versions | | Pillow version | | processors |
|
||||||
+==================================+===========================+==================+==============+
|
+==================================+===========================+==================+==============+
|
||||||
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.0 |arm |
|
| macOS 14 Sonoma | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
|
||||||
| +---------------------------+------------------+ |
|
|
||||||
| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 | |
|
|
||||||
+----------------------------------+---------------------------+------------------+--------------+
|
+----------------------------------+---------------------------+------------------+--------------+
|
||||||
| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
|
| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.1 |arm |
|
||||||
|
| +---------------------------+------------------+ |
|
||||||
|
| | 3.7 | 9.5.0 | |
|
||||||
|
+----------------------------------+---------------------------+------------------+--------------+
|
||||||
|
| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
|
||||||
+----------------------------------+---------------------------+------------------+--------------+
|
+----------------------------------+---------------------------+------------------+--------------+
|
||||||
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
|
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
|
||||||
| +---------------------------+------------------+--------------+
|
| +---------------------------+------------------+--------------+
|
||||||
|
|
|
@ -93,10 +93,14 @@ Generating images
|
||||||
Registering plugins
|
Registering plugins
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autofunction:: preinit
|
||||||
|
.. autofunction:: init
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
These functions are for use by plugin authors. Application authors can
|
These functions are for use by plugin authors. They are called when a
|
||||||
ignore them.
|
plugin is loaded as part of :py:meth:`~preinit()` or :py:meth:`~init()`.
|
||||||
|
Application authors can ignore them.
|
||||||
|
|
||||||
.. autofunction:: register_open
|
.. autofunction:: register_open
|
||||||
.. autofunction:: register_mime
|
.. autofunction:: register_mime
|
||||||
|
@ -347,6 +351,8 @@ Instances of the :py:class:`Image` class have the following attributes:
|
||||||
|
|
||||||
.. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell`
|
.. seealso:: :attr:`~Image.is_animated`, :func:`~Image.seek` and :func:`~Image.tell`
|
||||||
|
|
||||||
|
.. autoattribute:: PIL.Image.Image.has_transparency_data
|
||||||
|
|
||||||
Classes
|
Classes
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,11 @@ only work on L and RGB images.
|
||||||
|
|
||||||
.. autofunction:: autocontrast
|
.. autofunction:: autocontrast
|
||||||
.. autofunction:: colorize
|
.. autofunction:: colorize
|
||||||
.. autofunction:: contain
|
|
||||||
.. autofunction:: pad
|
|
||||||
.. autofunction:: crop
|
.. autofunction:: crop
|
||||||
.. autofunction:: scale
|
.. autofunction:: scale
|
||||||
.. autofunction:: deform
|
.. autofunction:: deform
|
||||||
.. autofunction:: equalize
|
.. autofunction:: equalize
|
||||||
.. autofunction:: expand
|
.. autofunction:: expand
|
||||||
.. autofunction:: fit
|
|
||||||
.. autofunction:: flip
|
.. autofunction:: flip
|
||||||
.. autofunction:: grayscale
|
.. autofunction:: grayscale
|
||||||
.. autofunction:: invert
|
.. autofunction:: invert
|
||||||
|
@ -27,3 +24,38 @@ only work on L and RGB images.
|
||||||
.. autofunction:: posterize
|
.. autofunction:: posterize
|
||||||
.. autofunction:: solarize
|
.. autofunction:: solarize
|
||||||
.. autofunction:: exif_transpose
|
.. autofunction:: exif_transpose
|
||||||
|
|
||||||
|
.. _relative-resize:
|
||||||
|
|
||||||
|
Resize relative to a given size
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
size = (100, 150)
|
||||||
|
with Image.open("Tests/images/hopper.png") as im:
|
||||||
|
ImageOps.contain(im, size).save("imageops_contain.png")
|
||||||
|
ImageOps.cover(im, size).save("imageops_cover.png")
|
||||||
|
ImageOps.fit(im, size).save("imageops_fit.png")
|
||||||
|
ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
|
||||||
|
|
||||||
|
# thumbnail() can also be used,
|
||||||
|
# but will modify the image object in place
|
||||||
|
im.thumbnail(size)
|
||||||
|
im.save("imageops_thumbnail.png")
|
||||||
|
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
| | :py:meth:`~PIL.Image.Image.thumbnail` | :py:meth:`~PIL.ImageOps.contain` | :py:meth:`~PIL.ImageOps.cover` | :py:meth:`~PIL.ImageOps.fit` | :py:meth:`~PIL.ImageOps.pad` |
|
||||||
|
+================+===========================================+============================================+==========================================+========================================+========================================+
|
||||||
|
|Given size | ``(150, 100)`` | ``(150, 100)`` | ``(150, 150)`` | ``(150, 100)`` | ``(150, 100)`` |
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
|Resulting size | ``100×100`` | ``100×100`` | ``150×150`` | ``150×100`` | ``150×100`` |
|
||||||
|
+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
|
||||||
|
|
||||||
|
.. autofunction:: contain
|
||||||
|
.. autofunction:: cover
|
||||||
|
.. autofunction:: fit
|
||||||
|
.. autofunction:: pad
|
||||||
|
|
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.
|
|
@ -13,42 +13,73 @@ not about removing existing functionality, but instead about raising an
|
||||||
explicit error to prevent later consequences. The ``convert`` method is the
|
explicit error to prevent later consequences. The ``convert`` method is the
|
||||||
correct way to change an image's mode.
|
correct way to change an image's mode.
|
||||||
|
|
||||||
Deprecations
|
|
||||||
============
|
|
||||||
|
|
||||||
TODO
|
|
||||||
^^^^
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
API Changes
|
API Changes
|
||||||
===========
|
===========
|
||||||
|
|
||||||
TODO
|
Accept a list in getpixel()
|
||||||
^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
:py:meth:`~PIL.Image.Image.getpixel` now accepts a list of coordinates, as well
|
||||||
|
as a tuple. ::
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
im = Image.new("RGB", (1, 1))
|
||||||
|
im.getpixel((0, 0))
|
||||||
|
im.getpixel([0, 0])
|
||||||
|
|
||||||
|
BoxBlur and GaussianBlur allow for different x and y radii
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
:py:class:`~PIL.ImageFilter.BoxBlur` and
|
||||||
|
:py:class:`~PIL.ImageFilter.GaussianBlur` now allow a sequence of x and y radii
|
||||||
|
to be specified, rather than a single number for both dimensions. ::
|
||||||
|
|
||||||
|
from PIL import ImageFilter
|
||||||
|
ImageFilter.BoxBlur((2, 5))
|
||||||
|
ImageFilter.GaussianBlur((2, 5))
|
||||||
|
|
||||||
API Additions
|
API Additions
|
||||||
=============
|
=============
|
||||||
|
|
||||||
TODO
|
ImageOps.cover
|
||||||
^^^^
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
Returns a resized version of the image, so that the requested size is covered,
|
||||||
|
while maintaining the original aspect ratio.
|
||||||
|
|
||||||
Security
|
See :ref:`relative-resize` for a comparison between this and similar ``ImageOps``
|
||||||
========
|
methods.
|
||||||
|
|
||||||
TODO
|
EpsImagePlugin.gs_binary
|
||||||
^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
``EpsImagePlugin.gs_windows_binary`` stores the name of the Ghostscript
|
||||||
|
executable on Windows. ``EpsImagePlugin.gs_binary`` has now been added for all
|
||||||
|
platforms, and can be used to customise the name of the executable, or disable
|
||||||
|
use entirely through ``EpsImagePlugin.gs_binary = False``.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
Other Changes
|
Other Changes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
TODO
|
Added support for DDS BC5U and 8-bit color indexed images
|
||||||
^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
Support has been added to read BC5U DDS files as RGB images, and
|
||||||
|
PALETTEINDEXED8 DDS files as P mode images.
|
||||||
|
|
||||||
|
Support reading signed 8-bit YCbCr TIFF images
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
TIFF images with unsigned integer data, 8 bits per sample and a photometric
|
||||||
|
interpretation of YCbCr can now be read.
|
||||||
|
|
|
@ -49,4 +49,3 @@ The external dependencies on libjpeg and zlib are now required by default.
|
||||||
If the headers or libraries are not found, then installation will abort
|
If the headers or libraries are not found, then installation will abort
|
||||||
with an error. This behaviour can be disabled with the ``--disable-libjpeg``
|
with an error. This behaviour can be disabled with the ``--disable-libjpeg``
|
||||||
and ``--disable-zlib`` flags.
|
and ``--disable-zlib`` flags.
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,3 @@ image size can lead to a smaller allocation than expected, leading to
|
||||||
arbitrary writes.
|
arbitrary writes.
|
||||||
|
|
||||||
This issue was found by Cris Neckar at Divergent Security.
|
This issue was found by Cris Neckar at Divergent Security.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,5 +20,3 @@ CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix
|
||||||
undefines PySlice_GetIndicesEx if it exists to restore compatibility
|
undefines PySlice_GetIndicesEx if it exists to restore compatibility
|
||||||
with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for
|
with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for
|
||||||
more details.
|
more details.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,4 +8,3 @@ Fixed Windows PyPy Build
|
||||||
|
|
||||||
A change in the 4.2.0 cycle broke the Windows PyPy build. This has
|
A change in the 4.2.0 cycle broke the Windows PyPy build. This has
|
||||||
been fixed, and PyPy is now part of the Windows CI matrix.
|
been fixed, and PyPy is now part of the Windows CI matrix.
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,3 @@ Dark theme for docs
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
|
The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ expected to be backported to earlier versions.
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
10.1.0
|
10.1.0
|
||||||
|
10.0.1
|
||||||
10.0.0
|
10.0.0
|
||||||
9.5.0
|
9.5.0
|
||||||
9.4.0
|
9.4.0
|
||||||
|
|
|
@ -68,11 +68,11 @@ def bdf_char(f):
|
||||||
# followed by the width in x (BBw), height in y (BBh),
|
# followed by the width in x (BBw), height in y (BBh),
|
||||||
# and x and y displacement (BBxoff0, BByoff0)
|
# and x and y displacement (BBxoff0, BByoff0)
|
||||||
# of the lower left corner from the origin of the character.
|
# of the lower left corner from the origin of the character.
|
||||||
width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()]
|
width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
|
||||||
|
|
||||||
# The word DWIDTH
|
# The word DWIDTH
|
||||||
# followed by the width in x and y of the character in device pixels.
|
# followed by the width in x and y of the character in device pixels.
|
||||||
dwx, dwy = [int(p) for p in props["DWIDTH"].split()]
|
dwx, dwy = (int(p) for p in props["DWIDTH"].split())
|
||||||
|
|
||||||
bbox = (
|
bbox = (
|
||||||
(dwx, dwy),
|
(dwx, dwy),
|
||||||
|
|
|
@ -419,9 +419,11 @@ class BLPEncoder(ImageFile.PyEncoder):
|
||||||
def _write_palette(self):
|
def _write_palette(self):
|
||||||
data = b""
|
data = b""
|
||||||
palette = self.im.getpalette("RGBA", "RGBA")
|
palette = self.im.getpalette("RGBA", "RGBA")
|
||||||
for i in range(256):
|
for i in range(len(palette) // 4):
|
||||||
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
r, g, b, a = palette[i * 4 : (i + 1) * 4]
|
||||||
data += struct.pack("<4B", b, g, r, a)
|
data += struct.pack("<4B", b, g, r, a)
|
||||||
|
while len(data) < 256 * 4:
|
||||||
|
data += b"\x00" * 4
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def encode(self, bufsize):
|
def encode(self, bufsize):
|
||||||
|
@ -442,7 +444,7 @@ class BLPEncoder(ImageFile.PyEncoder):
|
||||||
return len(data), 0, data
|
return len(data), 0, data
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename, save_all=False):
|
def _save(im, fp, filename):
|
||||||
if im.mode != "P":
|
if im.mode != "P":
|
||||||
msg = "Unsupported BLP image mode"
|
msg = "Unsupported BLP image mode"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
|
@ -13,7 +13,7 @@ Full text of the CC0 license:
|
||||||
import struct
|
import struct
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile, ImagePalette
|
||||||
from ._binary import o32le as o32
|
from ._binary import o32le as o32
|
||||||
|
|
||||||
# Magic ("DDS ")
|
# Magic ("DDS ")
|
||||||
|
@ -157,6 +157,10 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
|
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
|
||||||
|
|
||||||
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
|
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
|
||||||
|
elif pfflags & DDPF_PALETTEINDEXED8:
|
||||||
|
self._mode = "P"
|
||||||
|
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
|
||||||
|
self.tile = [("raw", (0, 0) + self.size, 0, "L")]
|
||||||
else:
|
else:
|
||||||
data_start = header_size + 4
|
data_start = header_size + 4
|
||||||
n = 0
|
n = 0
|
||||||
|
@ -173,7 +177,7 @@ class DdsImageFile(ImageFile.ImageFile):
|
||||||
self.pixel_format = "BC4"
|
self.pixel_format = "BC4"
|
||||||
n = 4
|
n = 4
|
||||||
self._mode = "L"
|
self._mode = "L"
|
||||||
elif fourcc == b"ATI2":
|
elif fourcc in (b"ATI2", b"BC5U"):
|
||||||
self.pixel_format = "BC5"
|
self.pixel_format = "BC5"
|
||||||
n = 5
|
n = 5
|
||||||
self._mode = "RGB"
|
self._mode = "RGB"
|
||||||
|
|
|
@ -37,8 +37,15 @@ from ._deprecate import deprecate
|
||||||
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
|
||||||
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
|
||||||
|
|
||||||
|
gs_binary = None
|
||||||
gs_windows_binary = None
|
gs_windows_binary = None
|
||||||
|
|
||||||
|
|
||||||
|
def has_ghostscript():
|
||||||
|
global gs_binary, gs_windows_binary
|
||||||
|
if gs_binary is None:
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
|
if gs_windows_binary is None:
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
for binary in ("gswin32c", "gswin64c", "gs"):
|
for binary in ("gswin32c", "gswin64c", "gs"):
|
||||||
|
@ -47,23 +54,22 @@ if sys.platform.startswith("win"):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
gs_windows_binary = False
|
gs_windows_binary = False
|
||||||
|
gs_binary = gs_windows_binary
|
||||||
|
else:
|
||||||
def has_ghostscript():
|
|
||||||
if gs_windows_binary:
|
|
||||||
return True
|
|
||||||
if not sys.platform.startswith("win"):
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
|
||||||
return True
|
gs_binary = "gs"
|
||||||
except OSError:
|
except OSError:
|
||||||
# No Ghostscript
|
gs_binary = False
|
||||||
pass
|
return gs_binary is not False
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||||
"""Render an image using Ghostscript"""
|
"""Render an image using Ghostscript"""
|
||||||
|
global gs_binary
|
||||||
|
if not has_ghostscript():
|
||||||
|
msg = "Unable to locate Ghostscript on paths"
|
||||||
|
raise OSError(msg)
|
||||||
|
|
||||||
# Unpack decoder tile
|
# Unpack decoder tile
|
||||||
decoder, tile, offset, data = tile[0]
|
decoder, tile, offset, data = tile[0]
|
||||||
|
@ -113,7 +119,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||||
|
|
||||||
# Build Ghostscript command
|
# Build Ghostscript command
|
||||||
command = [
|
command = [
|
||||||
"gs",
|
gs_binary,
|
||||||
"-q", # quiet mode
|
"-q", # quiet mode
|
||||||
"-g%dx%d" % size, # set output geometry (pixels)
|
"-g%dx%d" % size, # set output geometry (pixels)
|
||||||
"-r%fx%f" % res, # set input DPI (dots per inch)
|
"-r%fx%f" % res, # set input DPI (dots per inch)
|
||||||
|
@ -132,19 +138,6 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
||||||
"showpage",
|
"showpage",
|
||||||
]
|
]
|
||||||
|
|
||||||
if gs_windows_binary is not None:
|
|
||||||
if not gs_windows_binary:
|
|
||||||
try:
|
|
||||||
os.unlink(outfile)
|
|
||||||
if infile_temp:
|
|
||||||
os.unlink(infile_temp)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
msg = "Unable to locate Ghostscript on paths"
|
|
||||||
raise OSError(msg)
|
|
||||||
command[0] = gs_windows_binary
|
|
||||||
|
|
||||||
# push data through Ghostscript
|
# push data through Ghostscript
|
||||||
try:
|
try:
|
||||||
startupinfo = None
|
startupinfo = None
|
||||||
|
@ -233,7 +226,9 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
byte_arr = bytearray(255)
|
byte_arr = bytearray(255)
|
||||||
bytes_mv = memoryview(byte_arr)
|
bytes_mv = memoryview(byte_arr)
|
||||||
bytes_read = 0
|
bytes_read = 0
|
||||||
reading_comments = True
|
reading_header_comments = True
|
||||||
|
reading_trailer_comments = False
|
||||||
|
trailer_reached = False
|
||||||
|
|
||||||
def check_required_header_comments():
|
def check_required_header_comments():
|
||||||
if "PS-Adobe" not in self.info:
|
if "PS-Adobe" not in self.info:
|
||||||
|
@ -243,6 +238,36 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
msg = 'EPS header missing "%%BoundingBox" comment'
|
msg = 'EPS header missing "%%BoundingBox" comment'
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
def _read_comment(s):
|
||||||
|
nonlocal reading_trailer_comments
|
||||||
|
try:
|
||||||
|
m = split.match(s)
|
||||||
|
except re.error as e:
|
||||||
|
msg = "not an EPS file"
|
||||||
|
raise SyntaxError(msg) from e
|
||||||
|
|
||||||
|
if m:
|
||||||
|
k, v = m.group(1, 2)
|
||||||
|
self.info[k] = v
|
||||||
|
if k == "BoundingBox":
|
||||||
|
if v == "(atend)":
|
||||||
|
reading_trailer_comments = True
|
||||||
|
elif not self._size or (
|
||||||
|
trailer_reached and reading_trailer_comments
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# Note: The DSC spec says that BoundingBox
|
||||||
|
# fields should be integers, but some drivers
|
||||||
|
# put floating point values there anyway.
|
||||||
|
box = [int(float(i)) for i in v.split()]
|
||||||
|
self._size = box[2] - box[0], box[3] - box[1]
|
||||||
|
self.tile = [
|
||||||
|
("eps", (0, 0) + self.size, offset, (length, box))
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
byte = self.fp.read(1)
|
byte = self.fp.read(1)
|
||||||
if byte == b"":
|
if byte == b"":
|
||||||
|
@ -265,9 +290,9 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
msg = "not an EPS file"
|
msg = "not an EPS file"
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
else:
|
else:
|
||||||
if reading_comments:
|
if reading_header_comments:
|
||||||
check_required_header_comments()
|
check_required_header_comments()
|
||||||
reading_comments = False
|
reading_header_comments = False
|
||||||
# reset bytes_read so we can keep reading
|
# reset bytes_read so we can keep reading
|
||||||
# data until the end of the line
|
# data until the end of the line
|
||||||
bytes_read = 0
|
bytes_read = 0
|
||||||
|
@ -275,7 +300,7 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
bytes_read += 1
|
bytes_read += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if reading_comments:
|
if reading_header_comments:
|
||||||
# Load EPS header
|
# Load EPS header
|
||||||
|
|
||||||
# if this line doesn't start with a "%",
|
# if this line doesn't start with a "%",
|
||||||
|
@ -283,33 +308,11 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
# then we've reached the end of the header/comments
|
# then we've reached the end of the header/comments
|
||||||
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
|
if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments":
|
||||||
check_required_header_comments()
|
check_required_header_comments()
|
||||||
reading_comments = False
|
reading_header_comments = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
s = str(bytes_mv[:bytes_read], "latin-1")
|
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||||
|
if not _read_comment(s):
|
||||||
try:
|
|
||||||
m = split.match(s)
|
|
||||||
except re.error as e:
|
|
||||||
msg = "not an EPS file"
|
|
||||||
raise SyntaxError(msg) from e
|
|
||||||
|
|
||||||
if m:
|
|
||||||
k, v = m.group(1, 2)
|
|
||||||
self.info[k] = v
|
|
||||||
if k == "BoundingBox":
|
|
||||||
try:
|
|
||||||
# Note: The DSC spec says that BoundingBox
|
|
||||||
# fields should be integers, but some drivers
|
|
||||||
# put floating point values there anyway.
|
|
||||||
box = [int(float(i)) for i in v.split()]
|
|
||||||
self._size = box[2] - box[0], box[3] - box[1]
|
|
||||||
self.tile = [
|
|
||||||
("eps", (0, 0) + self.size, offset, (length, box))
|
|
||||||
]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
m = field.match(s)
|
m = field.match(s)
|
||||||
if m:
|
if m:
|
||||||
k = m.group(1)
|
k = m.group(1)
|
||||||
|
@ -339,9 +342,9 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
# data start identifier (the image data follows after a single line
|
# data start identifier (the image data follows after a single line
|
||||||
# consisting only of this quoted value)
|
# consisting only of this quoted value)
|
||||||
image_data_values = byte_arr[11:bytes_read].split(None, 7)
|
image_data_values = byte_arr[11:bytes_read].split(None, 7)
|
||||||
columns, rows, bit_depth, mode_id = [
|
columns, rows, bit_depth, mode_id = (
|
||||||
int(value) for value in image_data_values[:4]
|
int(value) for value in image_data_values[:4]
|
||||||
]
|
)
|
||||||
|
|
||||||
if bit_depth == 1:
|
if bit_depth == 1:
|
||||||
self._mode = "1"
|
self._mode = "1"
|
||||||
|
@ -355,7 +358,18 @@ class EpsImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
self._size = columns, rows
|
self._size = columns, rows
|
||||||
return
|
return
|
||||||
|
elif trailer_reached and reading_trailer_comments:
|
||||||
|
# Load EPS trailer
|
||||||
|
|
||||||
|
# if this line starts with "%%EOF",
|
||||||
|
# then we've reached the end of the file
|
||||||
|
if bytes_mv[:5] == b"%%EOF":
|
||||||
|
break
|
||||||
|
|
||||||
|
s = str(bytes_mv[:bytes_read], "latin-1")
|
||||||
|
_read_comment(s)
|
||||||
|
elif bytes_mv[:9] == b"%%Trailer":
|
||||||
|
trailer_reached = True
|
||||||
bytes_read = 0
|
bytes_read = 0
|
||||||
|
|
||||||
check_required_header_comments()
|
check_required_header_comments()
|
||||||
|
|
|
@ -298,7 +298,11 @@ _initialized = 0
|
||||||
|
|
||||||
|
|
||||||
def preinit():
|
def preinit():
|
||||||
"""Explicitly load standard file format drivers."""
|
"""
|
||||||
|
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.
|
||||||
|
|
||||||
|
It is called when opening or saving images.
|
||||||
|
"""
|
||||||
|
|
||||||
global _initialized
|
global _initialized
|
||||||
if _initialized >= 1:
|
if _initialized >= 1:
|
||||||
|
@ -334,11 +338,6 @@ def preinit():
|
||||||
assert PngImagePlugin
|
assert PngImagePlugin
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
# try:
|
|
||||||
# import TiffImagePlugin
|
|
||||||
# assert TiffImagePlugin
|
|
||||||
# except ImportError:
|
|
||||||
# pass
|
|
||||||
|
|
||||||
_initialized = 1
|
_initialized = 1
|
||||||
|
|
||||||
|
@ -347,6 +346,9 @@ def init():
|
||||||
"""
|
"""
|
||||||
Explicitly initializes the Python Imaging Library. This function
|
Explicitly initializes the Python Imaging Library. This function
|
||||||
loads all available file format drivers.
|
loads all available file format drivers.
|
||||||
|
|
||||||
|
It is called when opening or saving images if :py:meth:`~preinit()` is
|
||||||
|
insufficient, and by :py:meth:`~PIL.features.pilinfo`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
global _initialized
|
global _initialized
|
||||||
|
@ -913,7 +915,7 @@ class Image:
|
||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
has_transparency = self.info.get("transparency") is not None
|
has_transparency = "transparency" in self.info
|
||||||
if not mode and self.mode == "P":
|
if not mode and self.mode == "P":
|
||||||
# determine default mode
|
# determine default mode
|
||||||
if self.palette:
|
if self.palette:
|
||||||
|
@ -931,9 +933,9 @@ class Image:
|
||||||
msg = "illegal conversion"
|
msg = "illegal conversion"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
im = self.im.convert_matrix(mode, matrix)
|
im = self.im.convert_matrix(mode, matrix)
|
||||||
new = self._new(im)
|
new_im = self._new(im)
|
||||||
if has_transparency and self.im.bands == 3:
|
if has_transparency and self.im.bands == 3:
|
||||||
transparency = new.info["transparency"]
|
transparency = new_im.info["transparency"]
|
||||||
|
|
||||||
def convert_transparency(m, v):
|
def convert_transparency(m, v):
|
||||||
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
|
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5
|
||||||
|
@ -946,8 +948,8 @@ class Image:
|
||||||
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
|
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
|
||||||
for i in range(0, len(transparency))
|
for i in range(0, len(transparency))
|
||||||
)
|
)
|
||||||
new.info["transparency"] = transparency
|
new_im.info["transparency"] = transparency
|
||||||
return new
|
return new_im
|
||||||
|
|
||||||
if mode == "P" and self.mode == "RGBA":
|
if mode == "P" and self.mode == "RGBA":
|
||||||
return self.quantize(colors)
|
return self.quantize(colors)
|
||||||
|
@ -978,7 +980,7 @@ class Image:
|
||||||
else:
|
else:
|
||||||
# get the new transparency color.
|
# get the new transparency color.
|
||||||
# use existing conversions
|
# use existing conversions
|
||||||
trns_im = Image()._new(core.new(self.mode, (1, 1)))
|
trns_im = new(self.mode, (1, 1))
|
||||||
if self.mode == "P":
|
if self.mode == "P":
|
||||||
trns_im.putpalette(self.palette)
|
trns_im.putpalette(self.palette)
|
||||||
if isinstance(t, tuple):
|
if isinstance(t, tuple):
|
||||||
|
@ -1019,23 +1021,25 @@ class Image:
|
||||||
|
|
||||||
if mode == "P" and palette == Palette.ADAPTIVE:
|
if mode == "P" and palette == Palette.ADAPTIVE:
|
||||||
im = self.im.quantize(colors)
|
im = self.im.quantize(colors)
|
||||||
new = self._new(im)
|
new_im = self._new(im)
|
||||||
from . import ImagePalette
|
from . import ImagePalette
|
||||||
|
|
||||||
new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB"))
|
new_im.palette = ImagePalette.ImagePalette(
|
||||||
|
"RGB", new_im.im.getpalette("RGB")
|
||||||
|
)
|
||||||
if delete_trns:
|
if delete_trns:
|
||||||
# This could possibly happen if we requantize to fewer colors.
|
# This could possibly happen if we requantize to fewer colors.
|
||||||
# The transparency would be totally off in that case.
|
# The transparency would be totally off in that case.
|
||||||
del new.info["transparency"]
|
del new_im.info["transparency"]
|
||||||
if trns is not None:
|
if trns is not None:
|
||||||
try:
|
try:
|
||||||
new.info["transparency"] = new.palette.getcolor(trns, new)
|
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
|
||||||
except Exception:
|
except Exception:
|
||||||
# if we can't make a transparent color, don't leave the old
|
# if we can't make a transparent color, don't leave the old
|
||||||
# transparency hanging around to mess us up.
|
# transparency hanging around to mess us up.
|
||||||
del new.info["transparency"]
|
del new_im.info["transparency"]
|
||||||
warnings.warn("Couldn't allocate palette entry for transparency")
|
warnings.warn("Couldn't allocate palette entry for transparency")
|
||||||
return new
|
return new_im
|
||||||
|
|
||||||
if "LAB" in (self.mode, mode):
|
if "LAB" in (self.mode, mode):
|
||||||
other_mode = mode if self.mode == "LAB" else self.mode
|
other_mode = mode if self.mode == "LAB" else self.mode
|
||||||
|
@ -1072,7 +1076,7 @@ class Image:
|
||||||
if mode == "P" and palette != Palette.ADAPTIVE:
|
if mode == "P" and palette != Palette.ADAPTIVE:
|
||||||
from . import ImagePalette
|
from . import ImagePalette
|
||||||
|
|
||||||
new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
|
||||||
if delete_trns:
|
if delete_trns:
|
||||||
# crash fail if we leave a bytes transparency in an rgb/l mode.
|
# crash fail if we leave a bytes transparency in an rgb/l mode.
|
||||||
del new_im.info["transparency"]
|
del new_im.info["transparency"]
|
||||||
|
@ -1383,7 +1387,7 @@ class Image:
|
||||||
|
|
||||||
def _getxmp(self, xmp_tags):
|
def _getxmp(self, xmp_tags):
|
||||||
def get_name(tag):
|
def get_name(tag):
|
||||||
return tag.split("}")[1]
|
return re.sub("^{[^}]+}", "", tag)
|
||||||
|
|
||||||
def get_value(element):
|
def get_value(element):
|
||||||
value = {get_name(k): v for k, v in element.attrib.items()}
|
value = {get_name(k): v for k, v in element.attrib.items()}
|
||||||
|
@ -1529,6 +1533,24 @@ class Image:
|
||||||
rawmode = mode
|
rawmode = mode
|
||||||
return list(self.im.getpalette(mode, rawmode))
|
return list(self.im.getpalette(mode, rawmode))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_transparency_data(self) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if an image has transparency data, whether in the form of an
|
||||||
|
alpha channel, a palette with an alpha channel, or a "transparency" key
|
||||||
|
in the info dictionary.
|
||||||
|
|
||||||
|
Note the image might still appear solid, if all of the values shown
|
||||||
|
within are opaque.
|
||||||
|
|
||||||
|
:returns: A boolean.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.mode in ("LA", "La", "PA", "RGBA", "RGBa")
|
||||||
|
or (self.mode == "P" and self.palette.mode.endswith("A"))
|
||||||
|
or "transparency" in self.info
|
||||||
|
)
|
||||||
|
|
||||||
def apply_transparency(self):
|
def apply_transparency(self):
|
||||||
"""
|
"""
|
||||||
If a P mode image has a "transparency" key in the info dictionary,
|
If a P mode image has a "transparency" key in the info dictionary,
|
||||||
|
@ -1565,7 +1587,7 @@ class Image:
|
||||||
self.load()
|
self.load()
|
||||||
if self.pyaccess:
|
if self.pyaccess:
|
||||||
return self.pyaccess.getpixel(xy)
|
return self.pyaccess.getpixel(xy)
|
||||||
return self.im.getpixel(xy)
|
return self.im.getpixel(tuple(xy))
|
||||||
|
|
||||||
def getprojection(self):
|
def getprojection(self):
|
||||||
"""
|
"""
|
||||||
|
@ -3000,7 +3022,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
|
||||||
if args == ():
|
if args == ():
|
||||||
args = mode, 0, 1
|
args = mode, 0, 1
|
||||||
if args[0] in _MAPMODES:
|
if args[0] in _MAPMODES:
|
||||||
im = new(mode, (1, 1))
|
im = new(mode, (0, 0))
|
||||||
im = im._new(core.map_buffer(data, size, decoder_name, 0, args))
|
im = im._new(core.map_buffer(data, size, decoder_name, 0, args))
|
||||||
if mode == "P":
|
if mode == "P":
|
||||||
from . import ImagePalette
|
from . import ImagePalette
|
||||||
|
@ -3407,8 +3429,12 @@ def register_open(id, factory, accept=None):
|
||||||
|
|
||||||
def register_mime(id, mimetype):
|
def register_mime(id, mimetype):
|
||||||
"""
|
"""
|
||||||
Registers an image MIME type. This function should not be used
|
Registers an image MIME type by populating ``Image.MIME``. This function
|
||||||
in application code.
|
should not be used in application code.
|
||||||
|
|
||||||
|
``Image.MIME`` provides a mapping from image format identifiers to mime
|
||||||
|
formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can
|
||||||
|
provide a different result for specific images.
|
||||||
|
|
||||||
:param id: An image format identifier.
|
:param id: An image format identifier.
|
||||||
:param mimetype: The image MIME type for this format.
|
:param mimetype: The image MIME type for this format.
|
||||||
|
@ -3725,6 +3751,7 @@ class Exif(MutableMapping):
|
||||||
self.endian = self._info._endian
|
self.endian = self._info._endian
|
||||||
if offset is None:
|
if offset is None:
|
||||||
offset = self._info.next
|
offset = self._info.next
|
||||||
|
self.fp.tell()
|
||||||
self.fp.seek(offset)
|
self.fp.seek(offset)
|
||||||
self._info.load(self.fp)
|
self._info.load(self.fp)
|
||||||
|
|
||||||
|
|
|
@ -563,14 +563,21 @@ class FreeTypeFont:
|
||||||
if start is None:
|
if start is None:
|
||||||
start = (0, 0)
|
start = (0, 0)
|
||||||
im = None
|
im = None
|
||||||
|
size = None
|
||||||
|
|
||||||
def fill(mode, size):
|
def fill(mode, im_size):
|
||||||
nonlocal im
|
nonlocal im, size
|
||||||
|
|
||||||
|
size = im_size
|
||||||
|
if Image.MAX_IMAGE_PIXELS is not None:
|
||||||
|
pixels = max(1, size[0]) * max(1, size[1])
|
||||||
|
if pixels > 2 * Image.MAX_IMAGE_PIXELS:
|
||||||
|
return
|
||||||
|
|
||||||
im = Image.core.fill(mode, size)
|
im = Image.core.fill(mode, size)
|
||||||
return im
|
return im
|
||||||
|
|
||||||
size, offset = self.font.render(
|
offset = self.font.render(
|
||||||
text,
|
text,
|
||||||
fill,
|
fill,
|
||||||
mode,
|
mode,
|
||||||
|
@ -582,7 +589,6 @@ class FreeTypeFont:
|
||||||
ink,
|
ink,
|
||||||
start[0],
|
start[0],
|
||||||
start[1],
|
start[1],
|
||||||
Image.MAX_IMAGE_PIXELS,
|
|
||||||
)
|
)
|
||||||
Image._decompression_bomb_check(size)
|
Image._decompression_bomb_check(size)
|
||||||
return im, offset
|
return im, offset
|
||||||
|
|
|
@ -166,7 +166,7 @@ def grabclipboard():
|
||||||
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
|
msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.run(args, capture_output=True)
|
||||||
err = p.stderr
|
err = p.stderr
|
||||||
if err:
|
if err:
|
||||||
msg = f"{args[0]} error: {err.strip().decode()}"
|
msg = f"{args[0]} error: {err.strip().decode()}"
|
||||||
|
|
|
@ -242,7 +242,7 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
|
||||||
Returns a resized version of the image, set to the maximum width and height
|
Returns a resized version of the image, set to the maximum width and height
|
||||||
within the requested size, while maintaining the original aspect ratio.
|
within the requested size, while maintaining the original aspect ratio.
|
||||||
|
|
||||||
:param image: The image to resize and crop.
|
:param image: The image to resize.
|
||||||
:param size: The requested output size in pixels, given as a
|
:param size: The requested output size in pixels, given as a
|
||||||
(width, height) tuple.
|
(width, height) tuple.
|
||||||
:param method: Resampling method to use. Default is
|
:param method: Resampling method to use. Default is
|
||||||
|
@ -266,6 +266,35 @@ def contain(image, size, method=Image.Resampling.BICUBIC):
|
||||||
return image.resize(size, resample=method)
|
return image.resize(size, resample=method)
|
||||||
|
|
||||||
|
|
||||||
|
def cover(image, size, method=Image.Resampling.BICUBIC):
|
||||||
|
"""
|
||||||
|
Returns a resized version of the image, so that the requested size is
|
||||||
|
covered, while maintaining the original aspect ratio.
|
||||||
|
|
||||||
|
:param image: The image to resize.
|
||||||
|
:param size: The requested output size in pixels, given as a
|
||||||
|
(width, height) tuple.
|
||||||
|
:param method: Resampling method to use. Default is
|
||||||
|
:py:attr:`~PIL.Image.Resampling.BICUBIC`.
|
||||||
|
See :ref:`concept-filters`.
|
||||||
|
:return: An image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
im_ratio = image.width / image.height
|
||||||
|
dest_ratio = size[0] / size[1]
|
||||||
|
|
||||||
|
if im_ratio != dest_ratio:
|
||||||
|
if im_ratio < dest_ratio:
|
||||||
|
new_height = round(image.height / image.width * size[0])
|
||||||
|
if new_height != size[1]:
|
||||||
|
size = (size[0], new_height)
|
||||||
|
else:
|
||||||
|
new_width = round(image.width / image.height * size[1])
|
||||||
|
if new_width != size[0]:
|
||||||
|
size = (new_width, size[1])
|
||||||
|
return image.resize(size, resample=method)
|
||||||
|
|
||||||
|
|
||||||
def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
|
def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)):
|
||||||
"""
|
"""
|
||||||
Returns a resized and padded version of the image, expanded to fill the
|
Returns a resized and padded version of the image, expanded to fill the
|
||||||
|
@ -588,6 +617,7 @@ def exif_transpose(image, *, in_place=False):
|
||||||
with the transposition applied. If there is no transposition, a copy of the
|
with the transposition applied. If there is no transposition, a copy of the
|
||||||
image will be returned.
|
image will be returned.
|
||||||
"""
|
"""
|
||||||
|
image.load()
|
||||||
image_exif = image.getexif()
|
image_exif = image.getexif()
|
||||||
orientation = image_exif.get(ExifTags.Base.Orientation)
|
orientation = image_exif.get(ExifTags.Base.Orientation)
|
||||||
method = {
|
method = {
|
||||||
|
|
|
@ -58,13 +58,13 @@ class IptcImageFile(ImageFile.ImageFile):
|
||||||
#
|
#
|
||||||
# get a IPTC field header
|
# get a IPTC field header
|
||||||
s = self.fp.read(5)
|
s = self.fp.read(5)
|
||||||
if not len(s):
|
if not s.strip(b"\x00"):
|
||||||
return None, 0
|
return None, 0
|
||||||
|
|
||||||
tag = s[1], s[2]
|
tag = s[1], s[2]
|
||||||
|
|
||||||
# syntax
|
# syntax
|
||||||
if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9:
|
if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]:
|
||||||
msg = "invalid IPTC/NAA file"
|
msg = "invalid IPTC/NAA file"
|
||||||
raise SyntaxError(msg)
|
raise SyntaxError(msg)
|
||||||
|
|
||||||
|
|
|
@ -170,11 +170,19 @@ def APP(self, marker):
|
||||||
# 1 dpcm = 2.54 dpi
|
# 1 dpcm = 2.54 dpi
|
||||||
dpi *= 2.54
|
dpi *= 2.54
|
||||||
self.info["dpi"] = dpi, dpi
|
self.info["dpi"] = dpi, dpi
|
||||||
except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
except (
|
||||||
# SyntaxError for invalid/unreadable EXIF
|
struct.error,
|
||||||
|
KeyError,
|
||||||
|
SyntaxError,
|
||||||
|
TypeError,
|
||||||
|
ValueError,
|
||||||
|
ZeroDivisionError,
|
||||||
|
):
|
||||||
|
# struct.error for truncated EXIF
|
||||||
# KeyError for dpi not included
|
# KeyError for dpi not included
|
||||||
# ZeroDivisionError for invalid dpi rational value
|
# SyntaxError for invalid/unreadable EXIF
|
||||||
# ValueError or TypeError for dpi being an invalid float
|
# ValueError or TypeError for dpi being an invalid float
|
||||||
|
# ZeroDivisionError for invalid dpi rational value
|
||||||
self.info["dpi"] = 72, 72
|
self.info["dpi"] = 72, 72
|
||||||
|
|
||||||
|
|
||||||
|
@ -496,7 +504,7 @@ class JpegImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
for segment, content in self.applist:
|
for segment, content in self.applist:
|
||||||
if segment == "APP1":
|
if segment == "APP1":
|
||||||
marker, xmp_tags = content.rsplit(b"\x00", 1)
|
marker, xmp_tags = content.split(b"\x00")[:2]
|
||||||
if marker == b"http://ns.adobe.com/xap/1.0/":
|
if marker == b"http://ns.adobe.com/xap/1.0/":
|
||||||
return self._getxmp(xmp_tags)
|
return self._getxmp(xmp_tags)
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -1042,6 +1042,7 @@ _OUTMODES = {
|
||||||
"LA": ("LA", b"\x08\x04"),
|
"LA": ("LA", b"\x08\x04"),
|
||||||
"I": ("I;16B", b"\x10\x00"),
|
"I": ("I;16B", b"\x10\x00"),
|
||||||
"I;16": ("I;16B", b"\x10\x00"),
|
"I;16": ("I;16B", b"\x10\x00"),
|
||||||
|
"I;16B": ("I;16B", b"\x10\x00"),
|
||||||
"P;1": ("P;1", b"\x01\x03"),
|
"P;1": ("P;1", b"\x01\x03"),
|
||||||
"P;2": ("P;2", b"\x02\x03"),
|
"P;2": ("P;2", b"\x02\x03"),
|
||||||
"P;4": ("P;4", b"\x04\x03"),
|
"P;4": ("P;4", b"\x04\x03"),
|
||||||
|
@ -1103,9 +1104,6 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
|
||||||
for im_frame in ImageSequence.Iterator(im_seq):
|
for im_frame in ImageSequence.Iterator(im_seq):
|
||||||
if im_frame.mode == rawmode:
|
if im_frame.mode == rawmode:
|
||||||
im_frame = im_frame.copy()
|
im_frame = im_frame.copy()
|
||||||
else:
|
|
||||||
if rawmode == "P":
|
|
||||||
im_frame = im_frame.convert(rawmode, palette=im.palette)
|
|
||||||
else:
|
else:
|
||||||
im_frame = im_frame.convert(rawmode)
|
im_frame = im_frame.convert(rawmode)
|
||||||
encoderinfo = im.encoderinfo.copy()
|
encoderinfo = im.encoderinfo.copy()
|
||||||
|
@ -1166,6 +1164,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
|
||||||
|
|
||||||
# default image IDAT (if it exists)
|
# default image IDAT (if it exists)
|
||||||
if default_image:
|
if default_image:
|
||||||
|
if im.mode != rawmode:
|
||||||
|
im = im.convert(rawmode)
|
||||||
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
|
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
|
||||||
|
|
||||||
seq_num = 0
|
seq_num = 0
|
||||||
|
@ -1227,11 +1227,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
|
||||||
)
|
)
|
||||||
modes = set()
|
modes = set()
|
||||||
append_images = im.encoderinfo.get("append_images", [])
|
append_images = im.encoderinfo.get("append_images", [])
|
||||||
if default_image:
|
for im_seq in itertools.chain([im], append_images):
|
||||||
chain = itertools.chain(append_images)
|
|
||||||
else:
|
|
||||||
chain = itertools.chain([im], append_images)
|
|
||||||
for im_seq in chain:
|
|
||||||
for im_frame in ImageSequence.Iterator(im_seq):
|
for im_frame in ImageSequence.Iterator(im_seq):
|
||||||
modes.add(im_frame.mode)
|
modes.add(im_frame.mode)
|
||||||
for mode in ("RGBA", "RGB", "P"):
|
for mode in ("RGBA", "RGB", "P"):
|
||||||
|
|
|
@ -55,7 +55,7 @@ class QoiDecoder(ImageFile.PyDecoder):
|
||||||
while len(data) < self.state.xsize * self.state.ysize * bands:
|
while len(data) < self.state.xsize * self.state.ysize * bands:
|
||||||
byte = self.fd.read(1)[0]
|
byte = self.fd.read(1)[0]
|
||||||
if byte == 0b11111110: # QOI_OP_RGB
|
if byte == 0b11111110: # QOI_OP_RGB
|
||||||
value = self.fd.read(3) + o8(255)
|
value = self.fd.read(3) + self._previous_pixel[3:]
|
||||||
elif byte == 0b11111111: # QOI_OP_RGBA
|
elif byte == 0b11111111: # QOI_OP_RGBA
|
||||||
value = self.fd.read(4)
|
value = self.fd.read(4)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -251,6 +251,8 @@ OPEN_INFO = {
|
||||||
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
|
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
|
||||||
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
|
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
|
||||||
(II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
|
(II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
|
||||||
|
(II, 6, (1,), 1, (8,), ()): ("L", "L"),
|
||||||
|
(MM, 6, (1,), 1, (8,), ()): ("L", "L"),
|
||||||
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
|
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
|
||||||
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
|
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
|
||||||
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
|
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
|
||||||
|
@ -823,7 +825,7 @@ class ImageFileDirectory_v2(MutableMapping):
|
||||||
try:
|
try:
|
||||||
unit_size, handler = self._load_dispatch[typ]
|
unit_size, handler = self._load_dispatch[typ]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.debug(msg + f" - unsupported type {typ}")
|
logger.debug("%s - unsupported type %s", msg, typ)
|
||||||
continue # ignore unsupported type
|
continue # ignore unsupported type
|
||||||
size = count * unit_size
|
size = count * unit_size
|
||||||
if size > (8 if self._bigtiff else 4):
|
if size > (8 if self._bigtiff else 4):
|
||||||
|
@ -880,7 +882,7 @@ class ImageFileDirectory_v2(MutableMapping):
|
||||||
if tag == STRIPOFFSETS:
|
if tag == STRIPOFFSETS:
|
||||||
stripoffsets = len(entries)
|
stripoffsets = len(entries)
|
||||||
typ = self.tagtype.get(tag)
|
typ = self.tagtype.get(tag)
|
||||||
logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}")
|
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
|
||||||
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
|
is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
|
||||||
if is_ifd:
|
if is_ifd:
|
||||||
if self._endian == "<":
|
if self._endian == "<":
|
||||||
|
@ -929,7 +931,7 @@ class ImageFileDirectory_v2(MutableMapping):
|
||||||
|
|
||||||
# pass 2: write entries to file
|
# pass 2: write entries to file
|
||||||
for tag, typ, count, value, data in entries:
|
for tag, typ, count, value, data in entries:
|
||||||
logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}")
|
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
|
||||||
result += self._pack("HHL4s", tag, typ, count, value)
|
result += self._pack("HHL4s", tag, typ, count, value)
|
||||||
|
|
||||||
# -- overwrite here for multi-page --
|
# -- overwrite here for multi-page --
|
||||||
|
@ -1098,8 +1100,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
self._n_frames = None
|
self._n_frames = None
|
||||||
|
|
||||||
logger.debug("*** TiffImageFile._open ***")
|
logger.debug("*** TiffImageFile._open ***")
|
||||||
logger.debug(f"- __first: {self.__first}")
|
logger.debug("- __first: %s", self.__first)
|
||||||
logger.debug(f"- ifh: {repr(ifh)}") # Use repr to avoid str(bytes)
|
logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes)
|
||||||
|
|
||||||
# and load the first frame
|
# and load the first frame
|
||||||
self._seek(0)
|
self._seek(0)
|
||||||
|
@ -1137,12 +1139,15 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
msg = "no more images in TIFF file"
|
msg = "no more images in TIFF file"
|
||||||
raise EOFError(msg)
|
raise EOFError(msg)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Seeking to frame {frame}, on frame {self.__frame}, "
|
"Seeking to frame %s, on frame %s, __next %s, location: %s",
|
||||||
f"__next {self.__next}, location: {self.fp.tell()}"
|
frame,
|
||||||
|
self.__frame,
|
||||||
|
self.__next,
|
||||||
|
self.fp.tell(),
|
||||||
)
|
)
|
||||||
self.fp.seek(self.__next)
|
self.fp.seek(self.__next)
|
||||||
self._frame_pos.append(self.__next)
|
self._frame_pos.append(self.__next)
|
||||||
logger.debug("Loading tags, location: %s" % self.fp.tell())
|
logger.debug("Loading tags, location: %s", self.fp.tell())
|
||||||
self.tag_v2.load(self.fp)
|
self.tag_v2.load(self.fp)
|
||||||
if self.tag_v2.next in self._frame_pos:
|
if self.tag_v2.next in self._frame_pos:
|
||||||
# This IFD has already been processed
|
# This IFD has already been processed
|
||||||
|
@ -1203,20 +1208,6 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
return super().load()
|
return super().load()
|
||||||
|
|
||||||
def load_end(self):
|
def load_end(self):
|
||||||
if self._tile_orientation:
|
|
||||||
method = {
|
|
||||||
2: Image.Transpose.FLIP_LEFT_RIGHT,
|
|
||||||
3: Image.Transpose.ROTATE_180,
|
|
||||||
4: Image.Transpose.FLIP_TOP_BOTTOM,
|
|
||||||
5: Image.Transpose.TRANSPOSE,
|
|
||||||
6: Image.Transpose.ROTATE_270,
|
|
||||||
7: Image.Transpose.TRANSVERSE,
|
|
||||||
8: Image.Transpose.ROTATE_90,
|
|
||||||
}.get(self._tile_orientation)
|
|
||||||
if method is not None:
|
|
||||||
self.im = self.im.transpose(method)
|
|
||||||
self._size = self.im.size
|
|
||||||
|
|
||||||
# allow closing if we're on the first frame, there's no next
|
# allow closing if we're on the first frame, there's no next
|
||||||
# This is the ImageFile.load path only, libtiff specific below.
|
# This is the ImageFile.load path only, libtiff specific below.
|
||||||
if not self.is_animated:
|
if not self.is_animated:
|
||||||
|
@ -1233,6 +1224,10 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
continue
|
continue
|
||||||
exif.get_ifd(key)
|
exif.get_ifd(key)
|
||||||
|
|
||||||
|
ImageOps.exif_transpose(self, in_place=True)
|
||||||
|
if ExifTags.Base.Orientation in self.tag_v2:
|
||||||
|
del self.tag_v2[ExifTags.Base.Orientation]
|
||||||
|
|
||||||
def _load_libtiff(self):
|
def _load_libtiff(self):
|
||||||
"""Overload method triggered when we detect a compressed tiff
|
"""Overload method triggered when we detect a compressed tiff
|
||||||
Calls out to libtiff"""
|
Calls out to libtiff"""
|
||||||
|
@ -1340,18 +1335,18 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
fillorder = self.tag_v2.get(FILLORDER, 1)
|
fillorder = self.tag_v2.get(FILLORDER, 1)
|
||||||
|
|
||||||
logger.debug("*** Summary ***")
|
logger.debug("*** Summary ***")
|
||||||
logger.debug(f"- compression: {self._compression}")
|
logger.debug("- compression: %s", self._compression)
|
||||||
logger.debug(f"- photometric_interpretation: {photo}")
|
logger.debug("- photometric_interpretation: %s", photo)
|
||||||
logger.debug(f"- planar_configuration: {self._planar_configuration}")
|
logger.debug("- planar_configuration: %s", self._planar_configuration)
|
||||||
logger.debug(f"- fill_order: {fillorder}")
|
logger.debug("- fill_order: %s", fillorder)
|
||||||
logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")
|
logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING))
|
||||||
|
|
||||||
# size
|
# size
|
||||||
xsize = int(self.tag_v2.get(IMAGEWIDTH))
|
xsize = int(self.tag_v2.get(IMAGEWIDTH))
|
||||||
ysize = int(self.tag_v2.get(IMAGELENGTH))
|
ysize = int(self.tag_v2.get(IMAGELENGTH))
|
||||||
self._size = xsize, ysize
|
self._size = xsize, ysize
|
||||||
|
|
||||||
logger.debug(f"- size: {self.size}")
|
logger.debug("- size: %s", self.size)
|
||||||
|
|
||||||
sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
|
sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
|
||||||
if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
|
if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
|
||||||
|
@ -1407,7 +1402,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
bps_tuple,
|
bps_tuple,
|
||||||
extra_tuple,
|
extra_tuple,
|
||||||
)
|
)
|
||||||
logger.debug(f"format key: {key}")
|
logger.debug("format key: %s", key)
|
||||||
try:
|
try:
|
||||||
self._mode, rawmode = OPEN_INFO[key]
|
self._mode, rawmode = OPEN_INFO[key]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
|
@ -1415,8 +1410,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
msg = "unknown pixel mode"
|
msg = "unknown pixel mode"
|
||||||
raise SyntaxError(msg) from e
|
raise SyntaxError(msg) from e
|
||||||
|
|
||||||
logger.debug(f"- raw mode: {rawmode}")
|
logger.debug("- raw mode: %s", rawmode)
|
||||||
logger.debug(f"- pil mode: {self.mode}")
|
logger.debug("- pil mode: %s", self.mode)
|
||||||
|
|
||||||
self.info["compression"] = self._compression
|
self.info["compression"] = self._compression
|
||||||
|
|
||||||
|
@ -1457,7 +1452,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
if fillorder == 2:
|
if fillorder == 2:
|
||||||
# Replace fillorder with fillorder=1
|
# Replace fillorder with fillorder=1
|
||||||
key = key[:3] + (1,) + key[4:]
|
key = key[:3] + (1,) + key[4:]
|
||||||
logger.debug(f"format key: {key}")
|
logger.debug("format key: %s", key)
|
||||||
# this should always work, since all the
|
# this should always work, since all the
|
||||||
# fillorder==2 modes have a corresponding
|
# fillorder==2 modes have a corresponding
|
||||||
# fillorder=1 mode
|
# fillorder=1 mode
|
||||||
|
@ -1542,8 +1537,6 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
|
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
|
||||||
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
|
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
|
||||||
|
|
||||||
self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
@ -1622,7 +1615,7 @@ def _save(im, fp, filename):
|
||||||
info = exif
|
info = exif
|
||||||
else:
|
else:
|
||||||
info = {}
|
info = {}
|
||||||
logger.debug("Tiffinfo Keys: %s" % list(info))
|
logger.debug("Tiffinfo Keys: %s", list(info))
|
||||||
if isinstance(info, ImageFileDirectory_v1):
|
if isinstance(info, ImageFileDirectory_v1):
|
||||||
info = info.to_v2()
|
info = info.to_v2()
|
||||||
for key in info:
|
for key in info:
|
||||||
|
@ -1755,7 +1748,7 @@ def _save(im, fp, filename):
|
||||||
ifd[JPEGQUALITY] = quality
|
ifd[JPEGQUALITY] = quality
|
||||||
|
|
||||||
logger.debug("Saving using libtiff encoder")
|
logger.debug("Saving using libtiff encoder")
|
||||||
logger.debug("Items: %s" % sorted(ifd.items()))
|
logger.debug("Items: %s", sorted(ifd.items()))
|
||||||
_fp = 0
|
_fp = 0
|
||||||
if hasattr(fp, "fileno"):
|
if hasattr(fp, "fileno"):
|
||||||
try:
|
try:
|
||||||
|
@ -1823,7 +1816,7 @@ def _save(im, fp, filename):
|
||||||
if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
|
if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
|
||||||
atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
|
atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
|
||||||
|
|
||||||
logger.debug("Converted items: %s" % sorted(atts.items()))
|
logger.debug("Converted items: %s", sorted(atts.items()))
|
||||||
|
|
||||||
# libtiff always expects the bytes in native order.
|
# libtiff always expects the bytes in native order.
|
||||||
# we're storing image byte order. So, if the rawmode
|
# we're storing image byte order. So, if the rawmode
|
||||||
|
|
|
@ -74,9 +74,6 @@ class WebPImageFile(ImageFile.ImageFile):
|
||||||
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
|
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
|
||||||
self.n_frames = frame_count
|
self.n_frames = frame_count
|
||||||
self.is_animated = self.n_frames > 1
|
self.is_animated = self.n_frames > 1
|
||||||
ret = self._decoder.get_next()
|
|
||||||
if ret is not None:
|
|
||||||
self.info["duration"] = ret[1]
|
|
||||||
self._mode = "RGB" if mode == "RGBX" else mode
|
self._mode = "RGB" if mode == "RGBX" else mode
|
||||||
self.rawmode = mode
|
self.rawmode = mode
|
||||||
self.tile = []
|
self.tile = []
|
||||||
|
@ -93,7 +90,7 @@ class WebPImageFile(ImageFile.ImageFile):
|
||||||
self.info["xmp"] = xmp
|
self.info["xmp"] = xmp
|
||||||
|
|
||||||
# Initialize seek state
|
# Initialize seek state
|
||||||
self._reset()
|
self._reset(reset=False)
|
||||||
|
|
||||||
def _getexif(self):
|
def _getexif(self):
|
||||||
if "exif" not in self.info:
|
if "exif" not in self.info:
|
||||||
|
@ -116,7 +113,8 @@ class WebPImageFile(ImageFile.ImageFile):
|
||||||
# Set logical frame to requested position
|
# Set logical frame to requested position
|
||||||
self.__logical_frame = frame
|
self.__logical_frame = frame
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self, reset=True):
|
||||||
|
if reset:
|
||||||
self._decoder.reset()
|
self._decoder.reset()
|
||||||
self.__physical_frame = 0
|
self.__physical_frame = 0
|
||||||
self.__loaded = -1
|
self.__loaded = -1
|
||||||
|
@ -332,12 +330,7 @@ def _save(im, fp, filename):
|
||||||
exact = 1 if im.encoderinfo.get("exact") else 0
|
exact = 1 if im.encoderinfo.get("exact") else 0
|
||||||
|
|
||||||
if im.mode not in _VALID_WEBP_LEGACY_MODES:
|
if im.mode not in _VALID_WEBP_LEGACY_MODES:
|
||||||
alpha = (
|
im = im.convert("RGBA" if im.has_transparency_data else "RGB")
|
||||||
"A" in im.mode
|
|
||||||
or "a" in im.mode
|
|
||||||
or (im.mode == "P" and "transparency" in im.info)
|
|
||||||
)
|
|
||||||
im = im.convert("RGBA" if alpha else "RGB")
|
|
||||||
|
|
||||||
data = _webp.WebPEncode(
|
data = _webp.WebPEncode(
|
||||||
im.tobytes(),
|
im.tobytes(),
|
||||||
|
|
|
@ -475,8 +475,10 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) {
|
||||||
case IMAGING_TYPE_FLOAT32:
|
case IMAGING_TYPE_FLOAT32:
|
||||||
return PyFloat_FromDouble(pixel.f);
|
return PyFloat_FromDouble(pixel.f);
|
||||||
case IMAGING_TYPE_SPECIAL:
|
case IMAGING_TYPE_SPECIAL:
|
||||||
if (strncmp(im->mode, "I;16", 4) == 0) {
|
if (im->bands == 1) {
|
||||||
return PyLong_FromLong(pixel.h);
|
return PyLong_FromLong(pixel.h);
|
||||||
|
} else {
|
||||||
|
return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -599,7 +601,7 @@ getink(PyObject *color, Imaging im, char *ink) {
|
||||||
} else if (tupleSize != 3) {
|
} else if (tupleSize != 3) {
|
||||||
PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements");
|
PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements");
|
||||||
return NULL;
|
return NULL;
|
||||||
} else if (!PyArg_ParseTuple(color, "Lii", &r, &g, &b)) {
|
} else if (!PyArg_ParseTuple(color, "iiL", &b, &g, &r)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (!strcmp(im->mode, "BGR;15")) {
|
if (!strcmp(im->mode, "BGR;15")) {
|
||||||
|
@ -1571,23 +1573,48 @@ if (PySequence_Check(op)) { \
|
||||||
PyErr_SetString(PyExc_TypeError, must_be_sequence);
|
PyErr_SetString(PyExc_TypeError, must_be_sequence);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
int endian = strncmp(image->mode, "I;16", 4) == 0 ? (strcmp(image->mode, "I;16B") == 0 ? 2 : 1) : 0;
|
|
||||||
double value;
|
double value;
|
||||||
|
if (image->bands == 1) {
|
||||||
|
int bigendian = 0;
|
||||||
|
if (image->type == IMAGING_TYPE_SPECIAL) {
|
||||||
|
// I;16*
|
||||||
|
bigendian = strcmp(image->mode, "I;16B") == 0;
|
||||||
|
}
|
||||||
for (i = x = y = 0; i < n; i++) {
|
for (i = x = y = 0; i < n; i++) {
|
||||||
set_value_to_item(seq, i);
|
set_value_to_item(seq, i);
|
||||||
if (scale != 1.0 || offset != 0.0) {
|
if (scale != 1.0 || offset != 0.0) {
|
||||||
value = value * scale + offset;
|
value = value * scale + offset;
|
||||||
}
|
}
|
||||||
if (endian == 0) {
|
if (image->type == IMAGING_TYPE_SPECIAL) {
|
||||||
image->image8[y][x] = (UINT8)CLIP8(value);
|
image->image8[y][x * 2 + (bigendian ? 1 : 0)] = CLIP8((int)value % 256);
|
||||||
|
image->image8[y][x * 2 + (bigendian ? 0 : 1)] = CLIP8((int)value >> 8);
|
||||||
} else {
|
} else {
|
||||||
image->image8[y][x * 2 + (endian == 2 ? 1 : 0)] = CLIP8((int)value % 256);
|
image->image8[y][x] = (UINT8)CLIP8(value);
|
||||||
image->image8[y][x * 2 + (endian == 2 ? 0 : 1)] = CLIP8((int)value >> 8);
|
|
||||||
}
|
}
|
||||||
if (++x >= (int)image->xsize) {
|
if (++x >= (int)image->xsize) {
|
||||||
x = 0, y++;
|
x = 0, y++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// BGR;*
|
||||||
|
int b;
|
||||||
|
for (i = x = y = 0; i < n; i++) {
|
||||||
|
char ink[4];
|
||||||
|
|
||||||
|
op = PySequence_Fast_GET_ITEM(seq, i);
|
||||||
|
if (!op || !getink(op, image, ink)) {
|
||||||
|
Py_DECREF(seq);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
/* FIXME: what about scale and offset? */
|
||||||
|
for (b = 0; b < image->pixelsize; b++) {
|
||||||
|
image->image8[y][x * image->pixelsize + b] = ink[b];
|
||||||
|
}
|
||||||
|
if (++x >= (int)image->xsize) {
|
||||||
|
x = 0, y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
PyErr_Clear(); /* Avoid weird exceptions */
|
PyErr_Clear(); /* Avoid weird exceptions */
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -815,7 +815,6 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
float y_start = 0;
|
float y_start = 0;
|
||||||
int width, height, x_offset, y_offset;
|
int width, height, x_offset, y_offset;
|
||||||
int horizontal_dir; /* is primary axis horizontal? */
|
int horizontal_dir; /* is primary axis horizontal? */
|
||||||
PyObject *max_image_pixels = Py_None;
|
|
||||||
|
|
||||||
/* render string into given buffer (the buffer *must* have
|
/* render string into given buffer (the buffer *must* have
|
||||||
the right size, or this will crash) */
|
the right size, or this will crash) */
|
||||||
|
@ -833,8 +832,7 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
&anchor,
|
&anchor,
|
||||||
&foreground_ink_long,
|
&foreground_ink_long,
|
||||||
&x_start,
|
&x_start,
|
||||||
&y_start,
|
&y_start)) {
|
||||||
&max_image_pixels)) {
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -879,15 +877,11 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
|
|
||||||
width += stroke_width * 2 + ceil(x_start);
|
width += stroke_width * 2 + ceil(x_start);
|
||||||
height += stroke_width * 2 + ceil(y_start);
|
height += stroke_width * 2 + ceil(y_start);
|
||||||
if (max_image_pixels != Py_None) {
|
|
||||||
if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) {
|
|
||||||
PyMem_Del(glyph_info);
|
|
||||||
return Py_BuildValue("(ii)(ii)", width, height, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
|
image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height);
|
||||||
if (image == NULL) {
|
if (image == Py_None) {
|
||||||
|
PyMem_Del(glyph_info);
|
||||||
|
return Py_BuildValue("ii", 0, 0);
|
||||||
|
} else if (image == NULL) {
|
||||||
PyMem_Del(glyph_info);
|
PyMem_Del(glyph_info);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -898,7 +892,7 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
y_offset -= stroke_width;
|
y_offset -= stroke_width;
|
||||||
if (count == 0 || width == 0 || height == 0) {
|
if (count == 0 || width == 0 || height == 0) {
|
||||||
PyMem_Del(glyph_info);
|
PyMem_Del(glyph_info);
|
||||||
return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset);
|
return Py_BuildValue("ii", x_offset, y_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stroke_width) {
|
if (stroke_width) {
|
||||||
|
@ -1116,7 +1110,7 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
Py_DECREF(image);
|
Py_DECREF(image);
|
||||||
FT_Stroker_Done(stroker);
|
FT_Stroker_Done(stroker);
|
||||||
PyMem_Del(glyph_info);
|
PyMem_Del(glyph_info);
|
||||||
return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset);
|
return Py_BuildValue("ii", x_offset, y_offset);
|
||||||
|
|
||||||
glyph_error:
|
glyph_error:
|
||||||
if (im->destroy) {
|
if (im->destroy) {
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
#include "Imaging.h"
|
#include "Imaging.h"
|
||||||
|
|
||||||
/* use make_hash.py from the pillow-scripts repository to calculate these values */
|
/* use make_hash.py from the pillow-scripts repository to calculate these values */
|
||||||
#define ACCESS_TABLE_SIZE 27
|
#define ACCESS_TABLE_SIZE 35
|
||||||
#define ACCESS_TABLE_HASH 33051
|
#define ACCESS_TABLE_HASH 8940
|
||||||
|
|
||||||
static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE];
|
static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE];
|
||||||
|
|
||||||
|
@ -87,6 +87,31 @@ get_pixel_16(Imaging im, int x, int y, void *color) {
|
||||||
memcpy(color, in, sizeof(UINT16));
|
memcpy(color, in, sizeof(UINT16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
get_pixel_BGR15(Imaging im, int x, int y, void *color) {
|
||||||
|
UINT8 *in = (UINT8 *)&im->image8[y][x * 2];
|
||||||
|
UINT16 pixel = in[0] + (in[1] << 8);
|
||||||
|
char *out = color;
|
||||||
|
out[0] = (pixel & 31) * 255 / 31;
|
||||||
|
out[1] = ((pixel >> 5) & 31) * 255 / 31;
|
||||||
|
out[2] = ((pixel >> 10) & 31) * 255 / 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
get_pixel_BGR16(Imaging im, int x, int y, void *color) {
|
||||||
|
UINT8 *in = (UINT8 *)&im->image8[y][x * 2];
|
||||||
|
UINT16 pixel = in[0] + (in[1] << 8);
|
||||||
|
char *out = color;
|
||||||
|
out[0] = (pixel & 31) * 255 / 31;
|
||||||
|
out[1] = ((pixel >> 5) & 63) * 255 / 63;
|
||||||
|
out[2] = ((pixel >> 11) & 31) * 255 / 31;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
get_pixel_BGR24(Imaging im, int x, int y, void *color) {
|
||||||
|
memcpy(color, &im->image8[y][x * 3], sizeof(UINT8) * 3);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
get_pixel_32(Imaging im, int x, int y, void *color) {
|
get_pixel_32(Imaging im, int x, int y, void *color) {
|
||||||
memcpy(color, &im->image32[y][x], sizeof(INT32));
|
memcpy(color, &im->image32[y][x], sizeof(INT32));
|
||||||
|
@ -134,6 +159,16 @@ put_pixel_16B(Imaging im, int x, int y, const void *color) {
|
||||||
out[1] = in[0];
|
out[1] = in[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
put_pixel_BGR1516(Imaging im, int x, int y, const void *color) {
|
||||||
|
memcpy(&im->image8[y][x * 2], color, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
put_pixel_BGR24(Imaging im, int x, int y, const void *color) {
|
||||||
|
memcpy(&im->image8[y][x * 3], color, 3);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
put_pixel_32L(Imaging im, int x, int y, const void *color) {
|
put_pixel_32L(Imaging im, int x, int y, const void *color) {
|
||||||
memcpy(&im->image8[y][x * 4], color, 4);
|
memcpy(&im->image8[y][x * 4], color, 4);
|
||||||
|
@ -178,6 +213,9 @@ ImagingAccessInit() {
|
||||||
ADD("F", get_pixel_32, put_pixel_32);
|
ADD("F", get_pixel_32, put_pixel_32);
|
||||||
ADD("P", get_pixel_8, put_pixel_8);
|
ADD("P", get_pixel_8, put_pixel_8);
|
||||||
ADD("PA", get_pixel_32_2bands, put_pixel_32);
|
ADD("PA", get_pixel_32_2bands, put_pixel_32);
|
||||||
|
ADD("BGR;15", get_pixel_BGR15, put_pixel_BGR1516);
|
||||||
|
ADD("BGR;16", get_pixel_BGR16, put_pixel_BGR1516);
|
||||||
|
ADD("BGR;24", get_pixel_BGR24, put_pixel_BGR24);
|
||||||
ADD("RGB", get_pixel_32, put_pixel_32);
|
ADD("RGB", get_pixel_32, put_pixel_32);
|
||||||
ADD("RGBA", get_pixel_32, put_pixel_32);
|
ADD("RGBA", get_pixel_32, put_pixel_32);
|
||||||
ADD("RGBa", get_pixel_32, put_pixel_32);
|
ADD("RGBa", get_pixel_32, put_pixel_32);
|
||||||
|
|