diff --git a/.appveyor.yml b/.appveyor.yml
index 4e2ca1071..d525e4cfc 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -10,29 +10,29 @@ environment:
TEST_OPTIONS:
DEPLOY: YES
matrix:
- - PYTHON: C:/Python39
+ - PYTHON: C:/Python310
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
- - PYTHON: C:/Python36-x64
+ - PYTHON: C:/Python37-x64
ARCHITECTURE: x64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
install:
-- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip
+- '%PYTHON%\%EXECUTABLE% --version'
+- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
- 7z x pillow-depends.zip -oc:\
-- mv c:\pillow-depends-master c:\pillow-depends
+- mv c:\pillow-depends-main c:\pillow-depends
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
-- 7z x ..\pillow-depends\nasm-2.14.02-win64.zip -oc:\
-- ..\pillow-depends\gs9533w32.exe /S
-- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.3\bin;%PATH%
+- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
+- ..\pillow-depends\gs9550w32.exe /S
+- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH%
- cd c:\pillow\winbuild\
- ps: |
c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
c:\pillow\winbuild\build\build_dep_all.cmd
$host.SetShouldExit(0)
- path C:\pillow\winbuild\build\bin;%PATH%
-- '%PYTHON%\%EXECUTABLE% -m pip install -U setuptools'
build_script:
- ps: |
@@ -84,7 +84,7 @@ deploy:
artifact: /.*egg|wheel/
on:
APPVEYOR_REPO_NAME: python-pillow/Pillow
- branch: master
+ branch: main
deploy: YES
diff --git a/.ci/install.sh b/.ci/install.sh
index 4917b3a7c..c48acf9ee 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -22,24 +22,20 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
cmake imagemagick libharfbuzz-dev libfribidi-dev
python3 -m pip install --upgrade pip
+python3 -m pip install --upgrade wheel
PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
+python3 -m pip install defusedxml
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
python3 -m pip install test-image-results
-# TODO Remove condition when numpy supports 3.10
-if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
-
-# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+:
-if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
-if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
+python3 -m pip install numpy
# PyQt5 doesn't support PyPy3
-# Wheel doesn't yet support 3.10
-if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then
+if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
# arm64, ppc64le, s390x CPUs:
# "ERROR: Could not find a version that satisfies the requirement pyqt5"
sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
diff --git a/.ci/test.sh b/.ci/test.sh
index 9d2c123da..8ff7c5f64 100755
--- a/.ci/test.sh
+++ b/.ci/test.sh
@@ -4,4 +4,4 @@ set -e
python3 -c "from PIL import Image"
-python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests
+python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 35bd47be8..bc9587744 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -4,13 +4,13 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
## Bug fixes, feature additions, etc.
-Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
+Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
- Fork the Pillow repository.
-- Create a branch from master.
+- Create a branch from `main`.
- Develop bug fixes, features, tests, etc.
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
-- Create a pull request to pull the changes from your branch to the Pillow master.
+- Create a pull request to pull the changes from your branch to the Pillow `main`.
### Guidelines
@@ -18,7 +18,7 @@ Please send a pull request to the master branch. Please include [documentation](
- Provide tests for any newly added code.
- Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
-- Include [release notes](https://github.com/python-pillow/Pillow/tree/master/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
+- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
## Reporting Issues
@@ -35,4 +35,4 @@ The best reproductions are self-contained scripts with minimal dependencies. If
## Security vulnerabilities
-Please see our [security policy](https://github.com/python-pillow/Pillow/blob/master/.github/SECURITY.md).
+Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md).
diff --git a/.github/mergify.yml b/.github/mergify.yml
index 4b8b113d3..8b289bda6 100644
--- a/.github/mergify.yml
+++ b/.github/mergify.yml
@@ -7,6 +7,7 @@ pull_request_rules:
- status-success=Test Successful
- status-success=Docker Test Successful
- status-success=Windows Test Successful
+ - status-success=MinGW Test Successful
- status-success=continuous-integration/appveyor/pr
actions:
merge:
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
index 9fe8f774f..7e2fbf28f 100644
--- a/.github/workflows/cifuzz.yml
+++ b/.github/workflows/cifuzz.yml
@@ -1,4 +1,5 @@
name: CIFuzz
+
on:
push:
paths:
@@ -8,6 +9,7 @@ on:
paths:
- "**.c"
- "**.h"
+ workflow_dispatch:
jobs:
Fuzzing:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index bddeb6150..533ce8cbd 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,6 +1,6 @@
name: Lint
-on: [push, pull_request]
+on: [push, pull_request, workflow_dispatch]
jobs:
build:
@@ -12,14 +12,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- - name: pip cache
- uses: actions/cache@v2
- with:
- path: ~/.cache/pip
- key: lint-pip-${{ hashFiles('**/setup.py') }}
- restore-keys: |
- lint-pip-
-
- name: pre-commit cache
uses: actions/cache@v2
with:
@@ -31,7 +23,9 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
- python-version: 3.8
+ python-version: "3.10"
+ cache: pip
+ cache-dependency-path: "setup.py"
- name: Build system information
run: python3 .github/workflows/system-info.py
@@ -45,4 +39,3 @@ jobs:
run: tox -e lint
env:
PRE_COMMIT_COLOR: always
-
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index f45824445..8260cf8d8 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -6,6 +6,7 @@ brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype op
PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
+python3 -m pip install defusedxml
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
@@ -14,12 +15,7 @@ python3 -m pip install pyroma
python3 -m pip install test-image-results
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
-# TODO Remove condition when numpy supports 3.10
-if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi
-
-# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+:
-if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
-if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi
+python3 -m pip install numpy
# extra test images
pushd depends && ./install_extra_test_images.sh && popd
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
index 52456597b..ad66117b1 100644
--- a/.github/workflows/release-drafter.yml
+++ b/.github/workflows/release-drafter.yml
@@ -4,14 +4,15 @@ on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- - master
+ - main
+ workflow_dispatch:
jobs:
update_release_draft:
if: github.repository == 'python-pillow/Pillow'
runs-on: ubuntu-latest
steps:
- # Drafts your next release notes as pull requests are merged into "master"
+ # Drafts your next release notes as pull requests are merged into "main"
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index 2ecc27460..57396fddc 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -1,6 +1,6 @@
name: Test Docker
-on: [push, pull_request]
+on: [push, pull_request, workflow_dispatch]
jobs:
build:
@@ -20,13 +20,14 @@ jobs:
arch,
centos-7-amd64,
centos-8-amd64,
+ centos-stream-8-amd64,
debian-10-buster-x86,
- fedora-32-amd64,
- fedora-33-amd64,
+ fedora-34-amd64,
+ fedora-35-amd64,
ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
]
- dockerTag: [master]
+ dockerTag: [main]
include:
- docker: "ubuntu-20.04-focal-arm64v8"
qemu-arch: "aarch64"
diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml
new file mode 100644
index 000000000..d94c7d537
--- /dev/null
+++ b/.github/workflows/test-mingw.yml
@@ -0,0 +1,84 @@
+name: Test MinGW
+
+on: [push, pull_request, workflow_dispatch]
+
+jobs:
+ build:
+ runs-on: windows-2019
+ strategy:
+ fail-fast: false
+ matrix:
+ mingw: ["MINGW32", "MINGW64"]
+ include:
+ - mingw: "MINGW32"
+ name: "MSYS2 MinGW 32-bit"
+ package: "mingw-w64-i686"
+ - mingw: "MINGW64"
+ name: "MSYS2 MinGW 64-bit"
+ package: "mingw-w64-x86_64"
+
+ defaults:
+ run:
+ shell: bash.exe --login -eo pipefail "{0}"
+ env:
+ MSYSTEM: ${{ matrix.mingw }}
+ CHERE_INVOKING: 1
+
+ timeout-minutes: 30
+ name: ${{ matrix.name }}
+
+ steps:
+ - name: Checkout Pillow
+ uses: actions/checkout@v2
+
+ - name: Set up shell
+ run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
+ shell: pwsh
+
+ - name: Install dependencies
+ run: |
+ pacman -S --noconfirm \
+ ${{ matrix.package }}-python3-cffi \
+ ${{ matrix.package }}-python3-numpy \
+ ${{ matrix.package }}-python3-olefile \
+ ${{ matrix.package }}-python3-pip \
+ ${{ matrix.package }}-python-pyqt6 \
+ ${{ matrix.package }}-python3-setuptools \
+ ${{ matrix.package }}-freetype \
+ ${{ matrix.package }}-ghostscript \
+ ${{ matrix.package }}-lcms2 \
+ ${{ matrix.package }}-libimagequant \
+ ${{ matrix.package }}-libjpeg-turbo \
+ ${{ matrix.package }}-libraqm \
+ ${{ matrix.package }}-libtiff \
+ ${{ matrix.package }}-libwebp \
+ ${{ matrix.package }}-openjpeg2 \
+ subversion
+
+ python3 -m pip install pyroma pytest pytest-cov pytest-timeout
+
+ pushd depends && ./install_extra_test_images.sh && popd
+
+ - name: Build Pillow
+ run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" .
+
+ - name: Test Pillow
+ run: |
+ python3 selftest.py --installed
+ python3 -c "from PIL import Image"
+ python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
+
+ - name: Upload coverage
+ run: |
+ python3 -m pip install codecov
+ bash <(curl -s https://codecov.io/bash) -F GHA_Windows
+ env:
+ CODECOV_NAME: ${{ matrix.name }}
+
+ success:
+ needs: build
+ runs-on: ubuntu-latest
+ name: MinGW Test Successful
+ steps:
+ - name: Success
+ run: echo MinGW Test Successful
diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml
index 7b8474d0f..4a8966ca8 100644
--- a/.github/workflows/test-valgrind.yml
+++ b/.github/workflows/test-valgrind.yml
@@ -11,6 +11,7 @@ on:
paths:
- "**.c"
- "**.h"
+ workflow_dispatch:
jobs:
build:
@@ -22,7 +23,7 @@ jobs:
docker: [
ubuntu-20.04-focal-amd64-valgrind,
]
- dockerTag: [master]
+ dockerTag: [main]
name: ${{ matrix.docker }}
@@ -42,11 +43,3 @@ jobs:
sudo chown -R 1000 $GITHUB_WORKSPACE
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE
-
- success:
- needs: build
- runs-on: ubuntu-latest
- name: Valgrind Test Successful
- steps:
- - name: Success
- run: echo Valgrind Test Successful
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index e3b2201a7..c768838eb 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -1,6 +1,6 @@
name: Test Windows
-on: [push, pull_request]
+on: [push, pull_request, workflow_dispatch]
jobs:
build:
@@ -8,21 +8,15 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["pypy-3.6", "pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10-dev"]
+ python-version: ["3.7", "3.8", "3.9", "3.10"]
architecture: ["x86", "x64"]
include:
- - architecture: "x86"
- platform-vcvars: "x86"
- platform-msbuild: "Win32"
- - architecture: "x64"
- platform-vcvars: "x86_amd64"
- platform-msbuild: "x64"
- exclude:
- # PyPy does not support 64-bit on Windows
- - python-version: "pypy-3.6"
- architecture: "x64"
+ # PyPy 7.3.4+ only ships 64-bit binaries for Windows
- python-version: "pypy-3.7"
architecture: "x64"
+ - python-version: "pypy-3.8"
+ architecture: "x64"
+
timeout-minutes: 30
name: Python ${{ matrix.python-version }} ${{ matrix.architecture }}
@@ -37,42 +31,29 @@ jobs:
repository: python-pillow/pillow-depends
path: winbuild\depends
- - name: Cache pip
- uses: actions/cache@v2
- with:
- path: ~\AppData\Local\pip\Cache
- key:
- ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }}
- restore-keys: |
- ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-
- ${{ runner.os }}-${{ matrix.python-version }}-
-
# sets env: pythonLocation
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.architecture }}
+ cache: pip
+ cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information
run: python .github/workflows/system-info.py
- - name: python -m pip install wheel pytest pytest-cov pytest-timeout
- run: python -m pip install wheel pytest pytest-cov pytest-timeout
-
- # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+:
- - name: Upgrade setuptools
- if: "contains(matrix.python-version, '3.8') || contains(matrix.python-version, '3.9')"
- run: python -m pip install -U "setuptools>=49.3.2"
+ - name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
+ run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
- name: Install dependencies
id: install
run: |
- 7z x winbuild\depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\"
- echo "$env:RUNNER_WORKSPACE\nasm-2.14.02" >> $env:GITHUB_PATH
+ 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
+ echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
- winbuild\depends\gs9533w32.exe /S
- echo "C:\Program Files (x86)\gs\gs9.53.3\bin" >> $env:GITHUB_PATH
+ winbuild\depends\gs9550w32.exe /S
+ echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
@@ -151,7 +132,7 @@ jobs:
- name: Build Pillow
run: |
$FLAGS=""
- if ('${{ github.event_name }}' -eq 'push') { $FLAGS="--disable-imagequant" }
+ if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" }
& winbuild\build\build_pillow.cmd $FLAGS install
& $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh
@@ -194,92 +175,20 @@ jobs:
- name: Build wheel
id: wheel
- if: "github.event_name == 'push'"
+ if: "github.event_name != 'pull_request'"
run: |
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd
- uses: actions/upload-artifact@v2
- if: "github.event_name == 'push'"
+ if: "github.event_name != 'pull_request'"
with:
name: ${{ steps.wheel.outputs.dist }}
path: dist\*.whl
- msys:
- runs-on: windows-2019
-
- strategy:
- fail-fast: false
- matrix:
- mingw: ["MINGW32", "MINGW64"]
- include:
- - mingw: "MINGW32"
- name: "MSYS2 MinGW 32-bit"
- package: "mingw-w64-i686"
- - mingw: "MINGW64"
- name: "MSYS2 MinGW 64-bit"
- package: "mingw-w64-x86_64"
-
- defaults:
- run:
- shell: bash.exe --login -eo pipefail "{0}"
- env:
- MSYSTEM: ${{ matrix.mingw }}
- CHERE_INVOKING: 1
-
- timeout-minutes: 30
- name: ${{ matrix.name }}
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Set up shell
- run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
- shell: pwsh
-
- - name: Install Dependencies
- run: |
- pacman -S --noconfirm \
- ${{ matrix.package }}-python3-cffi \
- ${{ matrix.package }}-python3-numpy \
- ${{ matrix.package }}-python3-olefile \
- ${{ matrix.package }}-python3-pip \
- ${{ matrix.package }}-python3-pyqt5 \
- ${{ matrix.package }}-python3-setuptools \
- ${{ matrix.package }}-freetype \
- ${{ matrix.package }}-ghostscript \
- ${{ matrix.package }}-lcms2 \
- ${{ matrix.package }}-libimagequant \
- ${{ matrix.package }}-libjpeg-turbo \
- ${{ matrix.package }}-libraqm \
- ${{ matrix.package }}-libtiff \
- ${{ matrix.package }}-libwebp \
- ${{ matrix.package }}-openjpeg2 \
- subversion
-
- python3 -m pip install pyroma pytest pytest-cov
-
- pushd depends && ./install_extra_test_images.sh && popd
-
- - name: Build Pillow
- run: CFLAGS="-coverage" python3 setup.py build_ext install
-
- - name: Test Pillow
- run: |
- python3 selftest.py --installed
- python3 -c "from PIL import Image"
- python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
-
- - name: Upload coverage
- run: |
- python3 -m pip install codecov
- bash <(curl -s https://codecov.io/bash) -F GHA_Windows
- env:
- CODECOV_NAME: ${{ matrix.name }}
-
success:
- needs: [build, msys]
+ needs: build
runs-on: ubuntu-latest
name: Windows Test Successful
steps:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e52fefc69..414c7e94e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,6 +1,6 @@
name: Test
-on: [push, pull_request]
+on: [push, pull_request, workflow_dispatch]
jobs:
build:
@@ -9,27 +9,27 @@ jobs:
fail-fast: false
matrix:
os: [
+ "macos-latest",
"ubuntu-latest",
- "macOS-latest",
]
python-version: [
+ "pypy-3.8",
"pypy-3.7",
- "pypy-3.6",
- "3.10-dev",
+ "3.10",
"3.9",
"3.8",
"3.7",
- "3.6",
]
include:
- - python-version: "3.6"
- PYTHONOPTIMIZE: 1
- python-version: "3.7"
+ PYTHONOPTIMIZE: 1
+ REVERSE: "--reverse"
+ - python-version: "3.8"
PYTHONOPTIMIZE: 2
# Include new variables for Codecov
- os: ubuntu-latest
codecov-flag: GHA_Ubuntu
- - os: macOS-latest
+ - os: macos-latest
codecov-flag: GHA_macOS
runs-on: ${{ matrix.os }}
@@ -42,20 +42,8 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
-
- - name: Get pip cache dir
- id: pip-cache
- run: |
- echo "::set-output name=dir::$(python3 -m pip cache dir)"
-
- - name: pip cache
- uses: actions/cache@v2
- with:
- path: ${{ steps.pip-cache.outputs.dir }}
- key:
- ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.ci/*.sh') }}
- restore-keys: |
- ${{ matrix.os }}-${{ matrix.python-version }}-
+ cache: pip
+ cache-dependency-path: ".ci/*.sh"
- name: Build system information
run: python3 .github/workflows/system-info.py
@@ -71,8 +59,6 @@ jobs:
if: startsWith(matrix.os, 'macOS')
run: |
.github/workflows/macos-install.sh
- env:
- GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Build
run: |
@@ -80,6 +66,9 @@ jobs:
- name: Test
run: |
+ if [ $REVERSE ]; then
+ python3 -m pip install pytest-reverse
+ fi
if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then
xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
else
@@ -87,6 +76,7 @@ jobs:
fi
env:
PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }}
+ REVERSE: ${{ matrix.REVERSE }}
- name: Prepare to upload errors
if: failure()
@@ -103,7 +93,7 @@ jobs:
- name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9
run: |
- python3 -m pip install sphinx-issues sphinx-removed-in sphinx-rtd-theme
+ python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
make doccheck
- name: After success
diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml
new file mode 100644
index 000000000..c2b8b3bda
--- /dev/null
+++ b/.github/workflows/tidelift.yml
@@ -0,0 +1,26 @@
+name: Tidelift Align
+on:
+ schedule:
+ - cron: "30 2 * * *" # daily at 02:30 UTC
+ push:
+ paths:
+ - ".github/workflows/tidelift.yml"
+ pull_request:
+ paths:
+ - ".github/workflows/tidelift.yml"
+ workflow_dispatch:
+
+jobs:
+ build:
+ if: github.repository_owner == 'python-pillow'
+ name: Run Tidelift to ensure approved open source packages are in use
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Scan
+ uses: tidelift/alignment-action@main
+ env:
+ TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }}
+ TIDELIFT_ORGANIZATION: team/aclark4life
+ TIDELIFT_PROJECT: pillow
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8d38375f0..822fa43ca 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,43 +1,46 @@
repos:
- repo: https://github.com/psf/black
- rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1
+ rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0
hooks:
- id: black
- args: ["--target-version", "py36"]
+ args: ["--target-version", "py37"]
# Only .py files, until https://github.com/psf/black/issues/402 resolved
files: \.py$
types: []
- repo: https://github.com/PyCQA/isort
- rev: 377d260ffa6f746693f97b46d95025afc4bd8275 # frozen: 5.4.2
+ rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1 # frozen: 5.10.1
hooks:
- id: isort
- repo: https://github.com/asottile/yesqa
- rev: 7a009f3ee493c796827ee334f9058b110a0e0db8 # frozen: v1.2.1
+ rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10 # frozen: v1.3.0
hooks:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks
- rev: f30f4974a08a6b2f6a1eeaf30a4d501cf909163a # frozen: v1.1.9
+ rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10
hooks:
- id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- - repo: https://gitlab.com/pycqa/flake8
- rev: 05f6544aef321e2fee03a1277ce2eef8880fb927 # frozen: 3.8.3
+ - repo: https://github.com/PyCQA/flake8
+ rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d # frozen: 4.0.1
hooks:
- id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks
- rev: eae6397e4c259ed3d057511f6dd5330b92867e62 # frozen: v1.6.0
+ rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0
hooks:
- id: python-check-blanket-noqa
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: e1668fe86af3810fbca72b8653fe478e66a0afdc # frozen: v3.2.0
+ rev: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26 # frozen: v4.1.0
hooks:
- id: check-merge-conflict
- id: check-yaml
+
+ci:
+ autoupdate_schedule: quarterly
diff --git a/CHANGES.rst b/CHANGES.rst
index a2d40305f..c2d4892cb 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,414 @@
Changelog (Pillow)
==================
-8.2.0 (unreleased)
+9.0.0 (2022-01-02)
------------------
+- Restrict builtins for ImageMath.eval(). CVE TBD #5923
+ [radarhere]
+
+- Ensure JpegImagePlugin stops at the end of a truncated file #5921
+ [radarhere]
+
+- Fixed ImagePath.Path array handling. CVEs TBD #5920
+ [radarhere]
+
+- Remove consecutive duplicate tiles that only differ by their offset #5919
+ [radarhere]
+
+- Improved I;16 operations on big endian #5901
+ [radarhere]
+
+- Limit quantized palette to number of colors #5879
+ [radarhere]
+
+- Fixed palette index for zeroed color in FASTOCTREE quantize #5869
+ [radarhere]
+
+- When saving RGBA to GIF, make use of first transparent palette entry #5859
+ [radarhere]
+
+- Pass SAMPLEFORMAT to libtiff #5848
+ [radarhere]
+
+- Added rounding when converting P and PA #5824
+ [radarhere]
+
+- Improved putdata() documentation and data handling #5910
+ [radarhere]
+
+- Exclude carriage return in PDF regex to help prevent ReDoS #5912
+ [hugovk]
+
+- Fixed freeing pointer in ImageDraw.Outline.transform #5909
+ [radarhere]
+
+- Added ImageShow support for xdg-open #5897
+ [m-shinder, radarhere]
+
+- Support 16-bit grayscale ImageQt conversion #5856
+ [cmbruns, radarhere]
+
+- Convert subsequent GIF frames to RGB or RGBA #5857
+ [radarhere]
+
+- Do not prematurely return in ImageFile when saving to stdout #5665
+ [infmagic2047, radarhere]
+
+- Added support for top right and bottom right TGA orientations #5829
+ [radarhere]
+
+- Corrected ICNS file length in header #5845
+ [radarhere]
+
+- Block tile TIFF tags when saving #5839
+ [radarhere]
+
+- Added line width argument to polygon #5694
+ [radarhere]
+
+- Do not redeclare class each time when converting to NumPy #5844
+ [radarhere]
+
+- Only prevent repeated polygon pixels when drawing with transparency #5835
+ [radarhere]
+
+- Add support for pickling TrueType fonts #5826
+ [hugovk, radarhere]
+
+- Only prefer command line tools SDK on macOS over default MacOSX SDK #5828
+ [radarhere]
+
+- Drop support for soon-EOL Python 3.6 #5768
+ [hugovk, nulano, radarhere]
+
+- Fix compilation on 64-bit Termux #5793
+ [landfillbaby]
+
+- Use title for display in ImageShow #5788
+ [radarhere]
+
+- Remove support for FreeType 2.7 and older #5777
+ [hugovk, radarhere]
+
+- Fix for PyQt6 #5775
+ [hugovk, radarhere]
+
+- Removed deprecated PILLOW_VERSION, Image.show command parameter, Image._showxv and ImageFile.raise_ioerror #5776
+ [radarhere]
+
+8.4.0 (2021-10-15)
+------------------
+
+- Prefer global transparency in GIF when replacing with background color #5756
+ [radarhere]
+
+- Added "exif" keyword argument to TIFF saving #5575
+ [radarhere]
+
+- Copy Python palette to new image in quantize() #5696
+ [radarhere]
+
+- Read ICO AND mask from end #5667
+ [radarhere]
+
+- Actually check the framesize in FliDecode.c #5659
+ [wiredfool]
+
+- Determine JPEG2000 mode purely from ihdr header box #5654
+ [radarhere]
+
+- Fixed using info dictionary when writing multiple APNG frames #5611
+ [radarhere]
+
+- Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655
+ [radarhere]
+
+- For GIF save_all with palette, do not include palette with each frame #5603
+ [radarhere]
+
+- Keep transparency when converting from P to LA or PA #5606
+ [radarhere]
+
+- Copy palette to new image in transform() #5647
+ [radarhere]
+
+- Added "transparency" argument to EpsImagePlugin load() #5620
+ [radarhere]
+
+- Corrected pathlib.Path detection when saving #5633
+ [radarhere]
+
+- Added WalImageFile class #5618
+ [radarhere]
+
+- Consider I;16 pixel size when drawing text #5598
+ [radarhere]
+
+- If default conversion from P is RGB with transparency, convert to RGBA #5594
+ [radarhere]
+
+- Speed up rotating square images by 90 or 270 degrees #5646
+ [radarhere]
+
+- Add support for reading DPI information from JPEG2000 images
+ [rogermb, radarhere]
+
+- Catch TypeError from corrupted DPI value in EXIF #5639
+ [homm, radarhere]
+
+- Do not close file pointer when saving SGI images #5645
+ [farizrahman4u, radarhere]
+
+- Deprecate ImagePalette size parameter #5641
+ [radarhere, hugovk]
+
+- Prefer command line tools SDK on macOS #5624
+ [radarhere]
+
+- Added tags when saving YCbCr TIFF #5597
+ [radarhere]
+
+- PSD layer count may be negative #5613
+ [radarhere]
+
+- Fixed ImageOps expand with tuple border on P image #5615
+ [radarhere]
+
+- Fixed error saving APNG with duplicate frames and different duration times #5609
+ [thak1411, radarhere]
+
+8.3.2 (2021-09-02)
+------------------
+
+- CVE-2021-23437 Raise ValueError if color specifier is too long
+ [hugovk, radarhere]
+
+- Fix 6-byte OOB read in FliDecode
+ [wiredfool]
+
+- Add support for Python 3.10 #5569, #5570
+ [hugovk, radarhere]
+
+- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588
+ [kmilos, radarhere]
+
+- Updates for ``ImagePalette`` channel order #5599
+ [radarhere]
+
+- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651
+ [nulano]
+
+8.3.1 (2021-07-06)
+------------------
+
+- Catch OSError when checking if fp is sys.stdout #5585
+ [radarhere]
+
+- Handle removing orientation from alternate types of EXIF data #5584
+ [radarhere]
+
+- Make Image.__array__ take optional dtype argument #5572
+ [t-vi, radarhere]
+
+8.3.0 (2021-07-01)
+------------------
+
+- Use snprintf instead of sprintf. CVE-2021-34552 #5567
+ [radarhere]
+
+- Limit TIFF strip size when saving with LibTIFF #5514
+ [kmilos]
+
+- Allow ICNS save on all operating systems #4526
+ [baletu, radarhere, newpanjing, hugovk]
+
+- De-zigzag JPEG's DQT when loading; deprecate convert_dict_qtables #4989
+ [gofr, radarhere]
+
+- Replaced xml.etree.ElementTree #5565
+ [radarhere]
+
+- Moved CVE image to pillow-depends #5561
+ [radarhere]
+
+- Added tag data for IFD groups #5554
+ [radarhere]
+
+- Improved ImagePalette #5552
+ [radarhere]
+
+- Add DDS saving #5402
+ [radarhere]
+
+- Improved getxmp() #5455
+ [radarhere]
+
+- Convert to float for comparison with float in IFDRational __eq__ #5412
+ [radarhere]
+
+- Allow getexif() to access TIFF tag_v2 data #5416
+ [radarhere]
+
+- Read FITS image mode and size #5405
+ [radarhere]
+
+- Merge parallel horizontal edges in ImagingDrawPolygon #5347
+ [radarhere, hrdrq]
+
+- Use transparency behind first GIF frame and when disposing to background #5557
+ [radarhere, zewt]
+
+- Avoid unstable nature of qsort in Quant.c #5367
+ [radarhere]
+
+- Copy palette to new images in ImageOps expand #5551
+ [radarhere]
+
+- Ensure palette string matches RGB mode #5549
+ [radarhere]
+
+- Do not modify EXIF of original image instance in exif_transpose() #5547
+ [radarhere]
+
+- Fixed default numresolution for small JPEG2000 images #5540
+ [radarhere]
+
+- Added DDS BC5 reading #5501
+ [radarhere]
+
+- Raise an error if ImageDraw.textbbox is used without a TrueType font #5510
+ [radarhere]
+
+- Added ICO saving in BMP format #5513
+ [radarhere]
+
+- Ensure PNG seeks to end of previous chunk at start of load_end #5493
+ [radarhere]
+
+- Do not allow TIFF to seek to a past frame #5473
+ [radarhere]
+
+- Avoid race condition when displaying images with eog #5507
+ [mconst]
+
+- Added specific error messages when ink has incorrect number of bands #5504
+ [radarhere]
+
+- Allow converting an image to a numpy array to raise errors #5379
+ [radarhere]
+
+- Removed DPI rounding from BMP, JPEG, PNG and WMF loading #5476, #5470
+ [radarhere]
+
+- Remove spikes when drawing thin pieslices #5460
+ [xtsm]
+
+- Updated default value for SAMPLESPERPIXEL TIFF tag #5452
+ [radarhere]
+
+- Removed TIFF DPI rounding #5446
+ [radarhere, hugovk]
+
+- Include code in WebP error #5471
+ [radarhere]
+
+- Do not alter pixels outside mask when drawing text on an image with transparency #5434
+ [radarhere]
+
+- Reset handle when seeking backwards in TIFF #5443
+ [radarhere]
+
+- Replace sys.stdout with sys.stdout.buffer when saving #5437
+ [radarhere]
+
+- Fixed UNDEFINED TIFF tag of length 0 being changed in roundtrip #5426
+ [radarhere]
+
+- Fixed bug when checking FreeType2 version if it is not installed #5445
+ [radarhere]
+
+- Do not round dimensions when saving PDF #5459
+ [radarhere]
+
+- Added ImageOps contain() #5417
+ [radarhere, hugovk]
+
+- Changed WebP default "method" value to 4 #5450
+ [radarhere]
+
+- Switched to saving 1-bit PDFs with DCTDecode #5430
+ [radarhere]
+
+- Use bpp from ICO header #5429
+ [radarhere]
+
+- Corrected JPEG APP14 transform value #5408
+ [radarhere]
+
+- Changed TIFF tag 33723 length to 1 #5425
+ [radarhere]
+
+- Changed ImageMorph incorrect mode errors to ValueError #5414
+ [radarhere]
+
+- Add EXIF tags specified in EXIF 2.32 #5419
+ [gladiusglad]
+
+- Treat previous contents of first GIF frame as transparent #5391
+ [radarhere]
+
+- For special image modes, revert default resize resampling to NEAREST #5411
+ [radarhere]
+
+- JPEG2000: Support decoding subsampled RGB and YCbCr images #4996
+ [nulano, radarhere]
+
+- Stop decoding BC1 punchthrough alpha in BC2&3 #4144
+ [jansol]
+
+- Use zero if GIF background color index is missing #5390
+ [radarhere]
+
+- Fixed ensuring that GIF previous frame was loaded #5386
+ [radarhere]
+
+- Valgrind fixes #5397
+ [wiredfool]
+
+- Round down the radius in rounded_rectangle #5382
+ [radarhere]
+
+- Fixed reading uncompressed RGB data from DDS #5383
+ [radarhere]
+
+8.2.0 (2021-04-01)
+------------------
+
+- Added getxmp() method #5144
+ [UrielMaD, radarhere]
+
+- Add ImageShow support for GraphicsMagick #5349
+ [latosha-maltba, radarhere]
+
+- Do not load transparent pixels from subsequent GIF frames #5333
+ [zewt, radarhere]
+
+- Use LZW encoding when saving GIF images #5291
+ [raygard]
+
+- Set all transparent colors to be equal in quantize() #5282
+ [radarhere]
+
+- Allow PixelAccess to use Python __int__ when parsing x and y #5206
+ [radarhere]
+
+- Removed Image._MODEINFO #5316
+ [radarhere]
+
+- Add preserve_tone option to autocontrast #5350
+ [elejke, radarhere]
+
- Fixed linear_gradient and radial_gradient I and F modes #5274
[radarhere]
@@ -68,7 +473,7 @@ Changelog (Pillow)
- Changed Image.open formats parameter to be case-insensitive #5250
[Piolie, radarhere]
-- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-01-02) #5216
+- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-07-01) #5216
[radarhere]
- Added tk version to pilinfo #5226
diff --git a/LICENSE b/LICENSE
index 1197291bc..40aabc323 100644
--- a/LICENSE
+++ b/LICENSE
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2021 by Alex Clark and contributors
+ Copyright © 2010-2022 by Alex Clark and contributors
Like PIL, Pillow is licensed under the open source HPND License:
diff --git a/MANIFEST.in b/MANIFEST.in
index e9aaa8318..26f9401f2 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,7 @@
include *.c
include *.h
include *.in
+include *.lock
include *.md
include *.py
include *.rst
@@ -9,6 +10,7 @@ include *.txt
include *.yaml
include LICENSE
include Makefile
+include Pipfile
include tox.ini
graft Tests
graft src
diff --git a/Makefile b/Makefile
index 53eaa0566..0dac63d39 100644
--- a/Makefile
+++ b/Makefile
@@ -50,16 +50,16 @@ help:
.PHONY: inplace
inplace: clean
- python3 setup.py develop build_ext --inplace
+ python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" .
.PHONY: install
install:
- python3 setup.py install
+ python3 -m pip install .
python3 selftest.py
.PHONY: install-coverage
install-coverage:
- CFLAGS="-coverage -Werror=implicit-function-declaration" python3 setup.py build_ext install
+ CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" .
python3 selftest.py
.PHONY: debug
@@ -68,7 +68,7 @@ debug:
# for our stuff, kills optimization, and redirects to dev null so we
# see any build failures.
make clean > /dev/null
- CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null
+ CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null
.PHONY: install-req
install-req:
@@ -83,10 +83,10 @@ install-venv:
.PHONY: release-test
release-test:
$(MAKE) install-req
- python3 setup.py develop
+ python3 -m pip install -e .
python3 selftest.py
python3 -m pytest Tests
- python3 setup.py install
+ python3 -m pip install .
-rm dist/*.egg
-rmdir dist
python3 -m pytest -qq
@@ -96,12 +96,20 @@ release-test:
.PHONY: sdist
sdist:
- python3 setup.py sdist --format=gztar
+ python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
+ python3 -m build --sdist
.PHONY: test
test:
pytest -qq
+.PHONY: valgrind
+valgrind:
+ python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind
+ PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \
+ --log-file=/tmp/valgrind-output \
+ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output
+
.PHONY: readme
readme:
python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html
@@ -114,5 +122,5 @@ lint:
.PHONY: lint-fix
lint-fix:
- black --target-version py36 .
+ black --target-version py37 .
isort .
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 000000000..1e611a63c
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,22 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+black = "*"
+check-manifest = "*"
+coverage = "*"
+defusedxml = "*"
+packaging = "*"
+markdown2 = "*"
+olefile = "*"
+pyroma = "*"
+pytest = "*"
+pytest-cov = "*"
+pytest-timeout = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.9"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 000000000..600b19050
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,324 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.9"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "attrs": {
+ "hashes": [
+ "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
+ "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==21.2.0"
+ },
+ "black": {
+ "hashes": [
+ "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3",
+ "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"
+ ],
+ "index": "pypi",
+ "version": "==21.12b0"
+ },
+ "build": {
+ "hashes": [
+ "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f",
+ "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.7.0"
+ },
+ "certifi": {
+ "hashes": [
+ "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
+ "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
+ ],
+ "version": "==2021.10.8"
+ },
+ "charset-normalizer": {
+ "hashes": [
+ "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721",
+ "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"
+ ],
+ "markers": "python_version >= '3'",
+ "version": "==2.0.9"
+ },
+ "check-manifest": {
+ "hashes": [
+ "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95",
+ "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce"
+ ],
+ "index": "pypi",
+ "version": "==0.47"
+ },
+ "click": {
+ "hashes": [
+ "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3",
+ "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==8.0.3"
+ },
+ "coverage": {
+ "hashes": [
+ "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0",
+ "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd",
+ "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884",
+ "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48",
+ "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76",
+ "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0",
+ "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64",
+ "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685",
+ "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47",
+ "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d",
+ "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840",
+ "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f",
+ "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971",
+ "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c",
+ "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a",
+ "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de",
+ "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17",
+ "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4",
+ "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521",
+ "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57",
+ "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b",
+ "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282",
+ "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644",
+ "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475",
+ "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d",
+ "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da",
+ "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953",
+ "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2",
+ "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e",
+ "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c",
+ "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc",
+ "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64",
+ "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74",
+ "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617",
+ "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3",
+ "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d",
+ "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa",
+ "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739",
+ "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8",
+ "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8",
+ "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781",
+ "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58",
+ "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9",
+ "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c",
+ "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd",
+ "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e",
+ "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"
+ ],
+ "index": "pypi",
+ "version": "==6.2"
+ },
+ "defusedxml": {
+ "hashes": [
+ "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
+ "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
+ ],
+ "index": "pypi",
+ "version": "==0.7.1"
+ },
+ "docutils": {
+ "hashes": [
+ "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
+ "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==0.18.1"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
+ "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
+ ],
+ "markers": "python_version >= '3'",
+ "version": "==3.3"
+ },
+ "iniconfig": {
+ "hashes": [
+ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
+ "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
+ ],
+ "version": "==1.1.1"
+ },
+ "markdown2": {
+ "hashes": [
+ "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0",
+ "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e"
+ ],
+ "index": "pypi",
+ "version": "==2.4.2"
+ },
+ "mypy-extensions": {
+ "hashes": [
+ "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
+ "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
+ ],
+ "version": "==0.4.3"
+ },
+ "olefile": {
+ "hashes": [
+ "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"
+ ],
+ "index": "pypi",
+ "version": "==0.46"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
+ "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
+ ],
+ "index": "pypi",
+ "version": "==21.3"
+ },
+ "pathspec": {
+ "hashes": [
+ "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
+ "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
+ ],
+ "version": "==0.9.0"
+ },
+ "pep517": {
+ "hashes": [
+ "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0",
+ "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161"
+ ],
+ "version": "==0.12.0"
+ },
+ "platformdirs": {
+ "hashes": [
+ "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2",
+ "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2.4.0"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
+ "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==1.0.0"
+ },
+ "py": {
+ "hashes": [
+ "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
+ "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==1.11.0"
+ },
+ "pygments": {
+ "hashes": [
+ "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
+ "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==2.10.0"
+ },
+ "pyparsing": {
+ "hashes": [
+ "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4",
+ "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==3.0.6"
+ },
+ "pyroma": {
+ "hashes": [
+ "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65",
+ "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a"
+ ],
+ "index": "pypi",
+ "version": "==3.2"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
+ "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
+ ],
+ "index": "pypi",
+ "version": "==6.2.5"
+ },
+ "pytest-cov": {
+ "hashes": [
+ "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
+ "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
+ ],
+ "index": "pypi",
+ "version": "==3.0.0"
+ },
+ "pytest-timeout": {
+ "hashes": [
+ "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112",
+ "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717"
+ ],
+ "index": "pypi",
+ "version": "==2.0.2"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
+ "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+ "version": "==2.26.0"
+ },
+ "setuptools": {
+ "hashes": [
+ "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c",
+ "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==60.0.0"
+ },
+ "toml": {
+ "hashes": [
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+ ],
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
+ "version": "==0.10.2"
+ },
+ "tomli": {
+ "hashes": [
+ "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f",
+ "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==1.2.3"
+ },
+ "typing-extensions": {
+ "hashes": [
+ "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e",
+ "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==4.0.1"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
+ "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
+ "version": "==1.26.7"
+ }
+ },
+ "develop": {}
+}
diff --git a/README.md b/README.md
index 0408f4c28..782b81f33 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
# Pillow
@@ -38,13 +38,19 @@ As of 2019, Pillow development is
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg">
+ src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build">
+
+ alt="Travis CI wheels build status (aarch64)"
+ src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels">
+ src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg">
+
@@ -90,12 +96,12 @@ The core image library is designed for fast access to data stored in a few basic
- [Documentation](https://pillow.readthedocs.io/)
- [Installation](https://pillow.readthedocs.io/en/latest/installation.html)
- [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html)
-- [Contribute](https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md)
+- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md)
- [Issues](https://github.com/python-pillow/Pillow/issues)
- [Pull requests](https://github.com/python-pillow/Pillow/pulls)
- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html)
-- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- - [Pre-fork](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork)
+- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
+ - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
## Report a Vulnerability
diff --git a/RELEASING.md b/RELEASING.md
index 6045f84ac..cbedd449c 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -8,8 +8,8 @@ information about how the version numbers line up with releases.
Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
-* [ ] Develop and prepare release in `master` 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 `master` 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 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.
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`.
@@ -26,7 +26,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
make sdist
twine check dist/*
```
-* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions)
+* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
```bash
twine check dist/*
@@ -39,13 +39,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
Released as needed for security, installation or critical bug fixes.
-* [ ] Make necessary changes in `master` branch.
+* [ ] Make necessary changes in `main` branch.
* [ ] Update `CHANGES.rst`.
* [ ] Check out release branch e.g.:
```bash
git checkout -t remotes/origin/5.2.x
```
-* [ ] Cherry pick individual commits from `master` branch to release branch e.g. `5.2.x`, then `git push`.
+* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
@@ -63,7 +63,7 @@ Released as needed for security, installation or critical bug fixes.
make sdist
twine check dist/*
```
-* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions)
+* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
```bash
twine check dist/*
@@ -76,7 +76,7 @@ Released as needed for security, installation or critical bug fixes.
Released as needed privately to individual vendors for critical security-related bug fixes.
* [ ] Prepare patch for all versions that will get a fix. Test against local installations.
-* [ ] Commit against master, cherry pick to affected release branches.
+* [ ] Commit against `main`, cherry pick to affected release branches.
* [ ] Run local test matrix on each release & Python version.
* [ ] Privately send to distros.
* [ ] Run pre-release check via `make release-test`
@@ -93,7 +93,7 @@ Released as needed privately to individual vendors for critical security-related
make sdist
twine check dist/*
```
-* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions)
+* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
## Binary Distributions
diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py
index 26a91d5cd..e19cdf7a9 100755
--- a/Tests/32bit_segfault_check.py
+++ b/Tests/32bit_segfault_check.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import sys
diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py
index 739ad224e..7b3d4d7ee 100644
--- a/Tests/check_fli_oob.py
+++ b/Tests/check_fli_oob.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
from PIL import Image
@@ -61,8 +61,8 @@ repro_copy = (
for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy:
- im = Image.open(path)
- try:
- im.load()
- except Exception as msg:
- print(msg)
+ with Image.open(path) as im:
+ try:
+ im.load()
+ except Exception as msg:
+ print(msg)
diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py
index 407f3ea80..d07082aba 100755
--- a/Tests/check_imaging_leaks.py
+++ b/Tests/check_imaging_leaks.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import pytest
from PIL import Image
diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py
index a7a343c98..0210505f5 100755
--- a/Tests/check_jp2_overflow.py
+++ b/Tests/check_jp2_overflow.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Reproductions/tests for OOB read errors in FliDecode.c
@@ -19,8 +19,8 @@ from PIL import Image
repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2")
for path in repro:
- im = Image.open(path)
- try:
- im.load()
- except Exception as msg:
- print(msg)
+ with Image.open(path) as im:
+ try:
+ im.load()
+ except Exception as msg:
+ print(msg)
diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py
index 723a1a21e..c191ffc1e 100644
--- a/Tests/check_large_memory.py
+++ b/Tests/check_large_memory.py
@@ -33,7 +33,7 @@ def _write_png(tmp_path, xdim, ydim):
def test_large(tmp_path):
- """ succeeded prepatch"""
+ """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM)
diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py
index 79d1cfd5b..70ae6d230 100644
--- a/Tests/check_large_memory_numpy.py
+++ b/Tests/check_large_memory_numpy.py
@@ -31,7 +31,7 @@ def _write_png(tmp_path, xdim, ydim):
def test_large(tmp_path):
- """ succeeded prepatch"""
+ """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM)
diff --git a/Tests/conftest.py b/Tests/conftest.py
index 6f9945204..66da7593c 100644
--- a/Tests/conftest.py
+++ b/Tests/conftest.py
@@ -1,7 +1,4 @@
import io
-import warnings
-
-import pytest
def pytest_report_header(config):
@@ -16,16 +13,19 @@ def pytest_report_header(config):
def pytest_configure(config):
+ config.addinivalue_line(
+ "markers",
+ "pil_noop_mark: A conditional mark where nothing special happens",
+ )
+
# We're marking some tests to ignore valgrind errors and XFAIL them.
# Ensure that the mark is defined
# even in cases where pytest-valgrind isn't installed
-
- with warnings.catch_warnings():
- warnings.simplefilter("error")
- try:
- getattr(pytest.mark, "valgrind_known_error")
- except Exception:
- config.addinivalue_line(
- "markers",
- "valgrind_known_error: Tests that have known issues with valgrind",
- )
+ try:
+ config.addinivalue_line(
+ "markers",
+ "valgrind_known_error: Tests that have known issues with valgrind",
+ )
+ except Exception:
+ # valgrind is already installed
+ pass
diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py
index 011bb0bed..e318eb732 100755
--- a/Tests/createfontdatachunk.py
+++ b/Tests/createfontdatachunk.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import base64
import os
diff --git a/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf
new file mode 100644
index 000000000..790132515
Binary files /dev/null and b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf differ
diff --git a/Tests/helper.py b/Tests/helper.py
index be3bdb76f..feccce6bc 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -30,7 +30,6 @@ if os.environ.get("SHOW_ERRORS", None):
a.show()
b.show()
-
elif "GITHUB_ACTIONS" in os.environ:
HAS_UPLOADER = True
@@ -44,7 +43,6 @@ elif "GITHUB_ACTIONS" in os.environ:
b.save(os.path.join(tmpdir, "b.png"))
return tmpdir
-
else:
try:
import test_image_results
@@ -173,6 +171,21 @@ def skip_unless_feature_version(feature, version_required, reason=None):
return pytest.mark.skipif(version_available < version_required, reason=reason)
+def mark_if_feature_version(mark, feature, version_blacklist, reason=None):
+ if not features.check(feature):
+ return pytest.mark.pil_noop_mark()
+ if reason is None:
+ reason = f"{feature} is {version_blacklist}"
+ version_required = parse_version(version_blacklist)
+ version_available = parse_version(features.version(feature))
+ if (
+ version_available.major == version_required.major
+ and version_available.minor == version_required.minor
+ ):
+ return mark(reason=reason)
+ return pytest.mark.pil_noop_mark()
+
+
@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS")
class PillowLeakTestCase:
# requires unix/macOS
@@ -257,8 +270,23 @@ def netpbm_available():
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
-def imagemagick_available():
- return bool(IMCONVERT and shutil.which(IMCONVERT))
+def magick_command():
+ if sys.platform == "win32":
+ magickhome = os.environ.get("MAGICK_HOME", "")
+ if magickhome:
+ imagemagick = [os.path.join(magickhome, "convert.exe")]
+ graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"]
+ else:
+ imagemagick = None
+ graphicsmagick = None
+ else:
+ imagemagick = ["convert"]
+ graphicsmagick = ["gm", "convert"]
+
+ if imagemagick and shutil.which(imagemagick[0]):
+ return imagemagick
+ elif graphicsmagick and shutil.which(graphicsmagick[0]):
+ return graphicsmagick
def on_appveyor():
@@ -296,14 +324,6 @@ def is_mingw():
return sysconfig.get_platform() == "mingw"
-if sys.platform == "win32":
- IMCONVERT = os.environ.get("MAGICK_HOME", "")
- if IMCONVERT:
- IMCONVERT = os.path.join(IMCONVERT, "convert.exe")
-else:
- IMCONVERT = "convert"
-
-
class cached_property:
def __init__(self, func):
self.func = func
diff --git a/Tests/images/200x32_p_bl_raw_origin.tga b/Tests/images/200x32_p_bl_raw_origin.tga
new file mode 100644
index 000000000..329f0ca4d
Binary files /dev/null and b/Tests/images/200x32_p_bl_raw_origin.tga differ
diff --git a/Tests/images/balloon_eciRGBv2_aware.jp2 b/Tests/images/balloon_eciRGBv2_aware.jp2
new file mode 100644
index 000000000..18fd1e172
Binary files /dev/null and b/Tests/images/balloon_eciRGBv2_aware.jp2 differ
diff --git a/Tests/images/bc5_snorm.dds b/Tests/images/bc5_snorm.dds
new file mode 100644
index 000000000..7458c67c6
Binary files /dev/null and b/Tests/images/bc5_snorm.dds differ
diff --git a/Tests/images/bc5_typeless.dds b/Tests/images/bc5_typeless.dds
new file mode 100644
index 000000000..b5bae52bb
Binary files /dev/null and b/Tests/images/bc5_typeless.dds differ
diff --git a/Tests/images/bc5_unorm.dds b/Tests/images/bc5_unorm.dds
new file mode 100644
index 000000000..a04a026eb
Binary files /dev/null and b/Tests/images/bc5_unorm.dds differ
diff --git a/Tests/images/bc5_unorm.png b/Tests/images/bc5_unorm.png
new file mode 100644
index 000000000..05279ddfb
Binary files /dev/null and b/Tests/images/bc5_unorm.png differ
diff --git a/Tests/images/bc5s.dds b/Tests/images/bc5s.dds
new file mode 100644
index 000000000..0b999eed3
Binary files /dev/null and b/Tests/images/bc5s.dds differ
diff --git a/Tests/images/bc5s.png b/Tests/images/bc5s.png
new file mode 100644
index 000000000..39d7811bf
Binary files /dev/null and b/Tests/images/bc5s.png differ
diff --git a/Tests/images/bitmap_font_stroke_basic.png b/Tests/images/bitmap_font_stroke_basic.png
new file mode 100644
index 000000000..86b2d09f6
Binary files /dev/null and b/Tests/images/bitmap_font_stroke_basic.png differ
diff --git a/Tests/images/bitmap_font_stroke_raqm.png b/Tests/images/bitmap_font_stroke_raqm.png
new file mode 100644
index 000000000..08029ce34
Binary files /dev/null and b/Tests/images/bitmap_font_stroke_raqm.png differ
diff --git a/Tests/images/black_and_white.ico b/Tests/images/black_and_white.ico
new file mode 100644
index 000000000..f98d7ac8e
Binary files /dev/null and b/Tests/images/black_and_white.ico differ
diff --git a/Tests/images/broken_exif_dpi.jpg b/Tests/images/broken_exif_dpi.jpg
new file mode 100644
index 000000000..2c88b9463
Binary files /dev/null and b/Tests/images/broken_exif_dpi.jpg differ
diff --git a/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif
new file mode 100644
index 000000000..6e4e9b9ca
Binary files /dev/null and b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif differ
diff --git a/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k
new file mode 100644
index 000000000..c9bd7fc0a
Binary files /dev/null and b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k differ
diff --git a/Tests/images/crash-5762152299364352.fli b/Tests/images/crash-5762152299364352.fli
new file mode 100644
index 000000000..944fe0b56
Binary files /dev/null and b/Tests/images/crash-5762152299364352.fli differ
diff --git a/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif
new file mode 100644
index 000000000..053e4e4e9
Binary files /dev/null and b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif differ
diff --git a/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k
new file mode 100644
index 000000000..fd2f4dd36
Binary files /dev/null and b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k differ
diff --git a/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k
new file mode 100644
index 000000000..c3ad0d633
Binary files /dev/null and b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k differ
diff --git a/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k
new file mode 100644
index 000000000..3aadfc377
Binary files /dev/null and b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k differ
diff --git a/Tests/images/different_transparency.gif b/Tests/images/different_transparency.gif
new file mode 100644
index 000000000..2d36bef9e
Binary files /dev/null and b/Tests/images/different_transparency.gif differ
diff --git a/Tests/images/different_transparency_merged.png b/Tests/images/different_transparency_merged.png
new file mode 100644
index 000000000..3438f62a6
Binary files /dev/null and b/Tests/images/different_transparency_merged.png differ
diff --git a/Tests/images/dispose_bgnd_rgba.gif b/Tests/images/dispose_bgnd_rgba.gif
new file mode 100644
index 000000000..c18a0ba71
Binary files /dev/null and b/Tests/images/dispose_bgnd_rgba.gif differ
diff --git a/Tests/images/dispose_bgnd_transparency.gif b/Tests/images/dispose_bgnd_transparency.gif
new file mode 100644
index 000000000..7c626fe72
Binary files /dev/null and b/Tests/images/dispose_bgnd_transparency.gif differ
diff --git a/Tests/images/dispose_none_load_end_second.gif b/Tests/images/dispose_none_load_end_second.gif
deleted file mode 100644
index 5d8462ceb..000000000
Binary files a/Tests/images/dispose_none_load_end_second.gif and /dev/null differ
diff --git a/Tests/images/dispose_none_load_end_second.png b/Tests/images/dispose_none_load_end_second.png
new file mode 100644
index 000000000..dc01ccbdd
Binary files /dev/null and b/Tests/images/dispose_none_load_end_second.png differ
diff --git a/Tests/images/dispose_prev_first_frame.gif b/Tests/images/dispose_prev_first_frame.gif
new file mode 100644
index 000000000..4c19dd1ed
Binary files /dev/null and b/Tests/images/dispose_prev_first_frame.gif differ
diff --git a/Tests/images/dispose_prev_first_frame_seeked.png b/Tests/images/dispose_prev_first_frame_seeked.png
new file mode 100644
index 000000000..85a3753e1
Binary files /dev/null and b/Tests/images/dispose_prev_first_frame_seeked.png differ
diff --git a/Tests/images/drawing_roundDown.emf b/Tests/images/drawing_roundDown.emf
deleted file mode 100644
index 6c3e20248..000000000
Binary files a/Tests/images/drawing_roundDown.emf and /dev/null differ
diff --git a/Tests/images/dxt5-colorblock-alpha-issue-4142.dds b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds
new file mode 100644
index 000000000..905527ead
Binary files /dev/null and b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds differ
diff --git a/Tests/images/exif_imagemagick_orientation.png b/Tests/images/exif_imagemagick_orientation.png
new file mode 100644
index 000000000..819a0703f
Binary files /dev/null and b/Tests/images/exif_imagemagick_orientation.png differ
diff --git a/Tests/images/expected_to_read.jp2 b/Tests/images/expected_to_read.jp2
new file mode 100644
index 000000000..d8029a0d3
Binary files /dev/null and b/Tests/images/expected_to_read.jp2 differ
diff --git a/Tests/images/first_frame_transparency.gif b/Tests/images/first_frame_transparency.gif
new file mode 100644
index 000000000..86dc0de64
Binary files /dev/null and b/Tests/images/first_frame_transparency.gif differ
diff --git a/Tests/images/hopper.dds b/Tests/images/hopper.dds
new file mode 100644
index 000000000..8b9af9ed9
Binary files /dev/null and b/Tests/images/hopper.dds differ
diff --git a/Tests/images/hopper_roundUp_2.tif b/Tests/images/hopper_float_dpi_2.tif
similarity index 100%
rename from Tests/images/hopper_roundUp_2.tif
rename to Tests/images/hopper_float_dpi_2.tif
diff --git a/Tests/images/hopper_roundUp_3.tif b/Tests/images/hopper_float_dpi_3.tif
similarity index 100%
rename from Tests/images/hopper_roundUp_3.tif
rename to Tests/images/hopper_float_dpi_3.tif
diff --git a/Tests/images/hopper_roundUp_None.tif b/Tests/images/hopper_float_dpi_None.tif
similarity index 100%
rename from Tests/images/hopper_roundUp_None.tif
rename to Tests/images/hopper_float_dpi_None.tif
diff --git a/Tests/images/hopper_mask.ico b/Tests/images/hopper_mask.ico
new file mode 100644
index 000000000..e8d66c689
Binary files /dev/null and b/Tests/images/hopper_mask.ico differ
diff --git a/Tests/images/hopper_mask.png b/Tests/images/hopper_mask.png
new file mode 100644
index 000000000..c7bd2f708
Binary files /dev/null and b/Tests/images/hopper_mask.png differ
diff --git a/Tests/images/hopper_naxis_zero.fits b/Tests/images/hopper_naxis_zero.fits
new file mode 100644
index 000000000..580cf3a2c
Binary files /dev/null and b/Tests/images/hopper_naxis_zero.fits differ
diff --git a/Tests/images/hopper_resized.gif b/Tests/images/hopper_resized.gif
new file mode 100644
index 000000000..f7be6c262
Binary files /dev/null and b/Tests/images/hopper_resized.gif differ
diff --git a/Tests/images/hopper_roundDown.bmp b/Tests/images/hopper_roundDown.bmp
deleted file mode 100644
index 62aada050..000000000
Binary files a/Tests/images/hopper_roundDown.bmp and /dev/null differ
diff --git a/Tests/images/hopper_roundDown_2.tif b/Tests/images/hopper_roundDown_2.tif
deleted file mode 100644
index ac8cd057d..000000000
Binary files a/Tests/images/hopper_roundDown_2.tif and /dev/null differ
diff --git a/Tests/images/hopper_roundDown_3.tif b/Tests/images/hopper_roundDown_3.tif
deleted file mode 100644
index 0542fab9a..000000000
Binary files a/Tests/images/hopper_roundDown_3.tif and /dev/null differ
diff --git a/Tests/images/hopper_roundDown_None.tif b/Tests/images/hopper_roundDown_None.tif
deleted file mode 100644
index 21c40e8fe..000000000
Binary files a/Tests/images/hopper_roundDown_None.tif and /dev/null differ
diff --git a/Tests/images/hopper_wal.png b/Tests/images/hopper_wal.png
new file mode 100644
index 000000000..b6067c219
Binary files /dev/null and b/Tests/images/hopper_wal.png differ
diff --git a/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png
new file mode 100644
index 000000000..beffed5b9
Binary files /dev/null and b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png differ
diff --git a/Tests/images/imagedraw/triangle_right_width.png b/Tests/images/imagedraw/triangle_right_width.png
new file mode 100644
index 000000000..57b73553a
Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width.png differ
diff --git a/Tests/images/imagedraw/triangle_right_width_no_fill.png b/Tests/images/imagedraw/triangle_right_width_no_fill.png
new file mode 100644
index 000000000..dd65be6be
Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width_no_fill.png differ
diff --git a/Tests/images/imagedraw_polygon_translucent.png b/Tests/images/imagedraw_polygon_translucent.png
new file mode 100644
index 000000000..da8d790a3
Binary files /dev/null and b/Tests/images/imagedraw_polygon_translucent.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png
new file mode 100644
index 000000000..59e55b2a1
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png
new file mode 100644
index 000000000..c4e54896b
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png differ
diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png
new file mode 100644
index 000000000..6b0f11fa6
Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png differ
diff --git a/Tests/images/invalid_header_length.jp2 b/Tests/images/invalid_header_length.jp2
new file mode 100644
index 000000000..c0c14f421
Binary files /dev/null and b/Tests/images/invalid_header_length.jp2 differ
diff --git a/Tests/images/iptc_roundDown.jpg b/Tests/images/iptc_roundDown.jpg
deleted file mode 100644
index f98206f18..000000000
Binary files a/Tests/images/iptc_roundDown.jpg and /dev/null differ
diff --git a/Tests/images/missing_background.gif b/Tests/images/missing_background.gif
new file mode 100644
index 000000000..550d68d81
Binary files /dev/null and b/Tests/images/missing_background.gif differ
diff --git a/Tests/images/missing_background_first_frame.png b/Tests/images/missing_background_first_frame.png
new file mode 100644
index 000000000..25237ba5d
Binary files /dev/null and b/Tests/images/missing_background_first_frame.png differ
diff --git a/Tests/images/multiline_text.png b/Tests/images/multiline_text.png
index ff1308c5e..e39c6586c 100644
Binary files a/Tests/images/multiline_text.png and b/Tests/images/multiline_text.png differ
diff --git a/Tests/images/multiline_text_center.png b/Tests/images/multiline_text_center.png
index f44d0783a..837c6382a 100644
Binary files a/Tests/images/multiline_text_center.png and b/Tests/images/multiline_text_center.png differ
diff --git a/Tests/images/multiline_text_right.png b/Tests/images/multiline_text_right.png
index 1b32d9167..58b3bdddd 100644
Binary files a/Tests/images/multiline_text_right.png and b/Tests/images/multiline_text_right.png differ
diff --git a/Tests/images/multiline_text_spacing.png b/Tests/images/multiline_text_spacing.png
index 3c3bc0f26..3b367c7dd 100644
Binary files a/Tests/images/multiline_text_spacing.png and b/Tests/images/multiline_text_spacing.png differ
diff --git a/Tests/images/multipage_multiple_frame_loop.tiff b/Tests/images/multipage_multiple_frame_loop.tiff
new file mode 100644
index 000000000..b6759b080
Binary files /dev/null and b/Tests/images/multipage_multiple_frame_loop.tiff differ
diff --git a/Tests/images/multipage_out_of_order.tiff b/Tests/images/multipage_out_of_order.tiff
new file mode 100644
index 000000000..1576a549b
Binary files /dev/null and b/Tests/images/multipage_out_of_order.tiff differ
diff --git a/Tests/images/multipage_single_frame_loop.tiff b/Tests/images/multipage_single_frame_loop.tiff
new file mode 100644
index 000000000..26f27c421
Binary files /dev/null and b/Tests/images/multipage_single_frame_loop.tiff differ
diff --git a/Tests/images/negative_layer_count.psd b/Tests/images/negative_layer_count.psd
new file mode 100644
index 000000000..b111c2d56
Binary files /dev/null and b/Tests/images/negative_layer_count.psd differ
diff --git a/Tests/images/not_enough_data.jp2 b/Tests/images/not_enough_data.jp2
new file mode 100644
index 000000000..2d28bb5e9
Binary files /dev/null and b/Tests/images/not_enough_data.jp2 differ
diff --git a/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif
new file mode 100644
index 000000000..d43ba9192
Binary files /dev/null and b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif differ
diff --git a/Tests/images/p_16.png b/Tests/images/p_16.png
new file mode 100644
index 000000000..e35886412
Binary files /dev/null and b/Tests/images/p_16.png differ
diff --git a/Tests/images/p_16.tga b/Tests/images/p_16.tga
new file mode 100644
index 000000000..2b2ca4c70
Binary files /dev/null and b/Tests/images/p_16.tga differ
diff --git a/Tests/images/padded_idat.png b/Tests/images/padded_idat.png
new file mode 100644
index 000000000..18c5a4990
Binary files /dev/null and b/Tests/images/padded_idat.png differ
diff --git a/Tests/images/pal8_offset.bmp b/Tests/images/pal8_offset.bmp
new file mode 100644
index 000000000..24be65f22
Binary files /dev/null and b/Tests/images/pal8_offset.bmp differ
diff --git a/Tests/images/palette_negative.png b/Tests/images/palette_negative.png
new file mode 100644
index 000000000..938a7285f
Binary files /dev/null and b/Tests/images/palette_negative.png differ
diff --git a/Tests/images/palette_sepia.png b/Tests/images/palette_sepia.png
new file mode 100644
index 000000000..f3fc93253
Binary files /dev/null and b/Tests/images/palette_sepia.png differ
diff --git a/Tests/images/palette_wedge.png b/Tests/images/palette_wedge.png
new file mode 100644
index 000000000..23fb7940d
Binary files /dev/null and b/Tests/images/palette_wedge.png differ
diff --git a/Tests/images/reqd_showpage_transparency.png b/Tests/images/reqd_showpage_transparency.png
new file mode 100644
index 000000000..3ce159d0f
Binary files /dev/null and b/Tests/images/reqd_showpage_transparency.png differ
diff --git a/Tests/images/rgb32rle_bottom_right.tga b/Tests/images/rgb32rle_bottom_right.tga
new file mode 100644
index 000000000..bd4609e9c
Binary files /dev/null and b/Tests/images/rgb32rle_bottom_right.tga differ
diff --git a/Tests/images/rgb32rle_top_right.tga b/Tests/images/rgb32rle_top_right.tga
new file mode 100644
index 000000000..78f9dc5df
Binary files /dev/null and b/Tests/images/rgb32rle_top_right.tga differ
diff --git a/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp
new file mode 100644
index 000000000..97def320f
Binary files /dev/null and b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp differ
diff --git a/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd
new file mode 100644
index 000000000..63319e545
Binary files /dev/null and b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd differ
diff --git a/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp
new file mode 100644
index 000000000..73022abfc
Binary files /dev/null and b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp differ
diff --git a/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd
new file mode 100644
index 000000000..c259a15e7
Binary files /dev/null and b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd differ
diff --git a/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp
new file mode 100644
index 000000000..79e97dce3
Binary files /dev/null and b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp differ
diff --git a/Tests/images/timeout-6646305047838720 b/Tests/images/timeout-6646305047838720
new file mode 100644
index 000000000..eae1f333a
Binary files /dev/null and b/Tests/images/timeout-6646305047838720 differ
diff --git a/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp
new file mode 100644
index 000000000..9b9ecbcb0
Binary files /dev/null and b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp differ
diff --git a/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli
new file mode 100644
index 000000000..ce4607d2d
Binary files /dev/null and b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli differ
diff --git a/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp
new file mode 100644
index 000000000..cb9a4e8b3
Binary files /dev/null and b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp differ
diff --git a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli
new file mode 100644
index 000000000..77a94b87a
Binary files /dev/null and b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli differ
diff --git a/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd
new file mode 100644
index 000000000..955fc3325
Binary files /dev/null and b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd differ
diff --git a/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps
new file mode 100644
index 000000000..5000ca9aa
Binary files /dev/null and b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps differ
diff --git a/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp
new file mode 100644
index 000000000..5044fbde1
Binary files /dev/null and b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp differ
diff --git a/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd
new file mode 100644
index 000000000..c658ea45c
Binary files /dev/null and b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd differ
diff --git a/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp
new file mode 100644
index 000000000..7ef78eeec
Binary files /dev/null and b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp differ
diff --git a/Tests/images/transparent_background_text.png b/Tests/images/transparent_background_text.png
index 40acd92b6..8ddd65cc6 100644
Binary files a/Tests/images/transparent_background_text.png and b/Tests/images/transparent_background_text.png differ
diff --git a/Tests/images/transparent_background_text_L.png b/Tests/images/transparent_background_text_L.png
new file mode 100644
index 000000000..d37de20a7
Binary files /dev/null and b/Tests/images/transparent_background_text_L.png differ
diff --git a/Tests/images/transparent_dispose.gif b/Tests/images/transparent_dispose.gif
new file mode 100644
index 000000000..92b615543
Binary files /dev/null and b/Tests/images/transparent_dispose.gif differ
diff --git a/Tests/images/truncated_app14.jpg b/Tests/images/truncated_app14.jpg
new file mode 100644
index 000000000..232a4c35f
Binary files /dev/null and b/Tests/images/truncated_app14.jpg differ
diff --git a/Tests/images/uncompressed_rgb.png b/Tests/images/uncompressed_rgb.png
index 50bca09ee..f02b50f6f 100644
Binary files a/Tests/images/uncompressed_rgb.png and b/Tests/images/uncompressed_rgb.png differ
diff --git a/Tests/images/xmp_test.jpg b/Tests/images/xmp_test.jpg
new file mode 100644
index 000000000..4b9354f3a
Binary files /dev/null and b/Tests/images/xmp_test.jpg differ
diff --git a/Tests/images/zero_dpi.jp2 b/Tests/images/zero_dpi.jp2
new file mode 100644
index 000000000..079271fc6
Binary files /dev/null and b/Tests/images/zero_dpi.jp2 differ
diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh
index 513136fff..09cc7bc16 100755
--- a/Tests/oss-fuzz/build.sh
+++ b/Tests/oss-fuzz/build.sh
@@ -22,7 +22,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
fuzzer_basename=$(basename -s .py $fuzzer)
fuzzer_package=${fuzzer_basename}.pkg
pyinstaller \
- --add-binary /usr/local/lib/libjpeg.so.9:. \
+ --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \
--add-binary /usr/local/lib/libfreetype.so.6:. \
--add-binary /usr/local/lib/liblcms2.so.2:. \
--add-binary /usr/local/lib/libopenjp2.so.7:. \
diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py
index bdfda7a13..bc2ba9a7e 100755
--- a/Tests/oss-fuzz/fuzz_font.py
+++ b/Tests/oss-fuzz/fuzz_font.py
@@ -14,10 +14,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import sys
-import atheris_no_libfuzzer as atheris
-import fuzzers
+import atheris
+
+with atheris.instrument_imports():
+ import sys
+
+ import fuzzers
def TestOneInput(data):
@@ -26,14 +29,14 @@ def TestOneInput(data):
except Exception:
# We're catching all exceptions because Pillow's exceptions are
# directly inheriting from Exception.
- return
- return
+ pass
def main():
fuzzers.enable_decompressionbomb_error()
- atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
+ atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
+ fuzzers.disable_decompressionbomb_error()
if __name__ == "__main__":
diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py
index d816d535f..545daccb6 100644
--- a/Tests/oss-fuzz/fuzz_pillow.py
+++ b/Tests/oss-fuzz/fuzz_pillow.py
@@ -14,10 +14,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import sys
-import atheris_no_libfuzzer as atheris
-import fuzzers
+import atheris
+
+with atheris.instrument_imports():
+ import sys
+
+ import fuzzers
def TestOneInput(data):
@@ -26,14 +29,14 @@ def TestOneInput(data):
except Exception:
# We're catching all exceptions because Pillow's exceptions are
# directly inheriting from Exception.
- return
- return
+ pass
def main():
fuzzers.enable_decompressionbomb_error()
- atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True)
+ atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
+ fuzzers.disable_decompressionbomb_error()
if __name__ == "__main__":
diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py
index 1e7a4e27d..5786764a6 100644
--- a/Tests/oss-fuzz/fuzzers.py
+++ b/Tests/oss-fuzz/fuzzers.py
@@ -10,6 +10,11 @@ def enable_decompressionbomb_error():
warnings.simplefilter("error", Image.DecompressionBombWarning)
+def disable_decompressionbomb_error():
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
+ warnings.resetwarnings()
+
+
def fuzz_image(data):
# This will fail on some images in the corpus, as we have many
# invalid images in the test suite.
diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp
new file mode 100644
index 000000000..94cc87db9
--- /dev/null
+++ b/Tests/oss-fuzz/python.supp
@@ -0,0 +1,16 @@
+{
+
+ Memcheck:Cond
+ ...
+ fun:encode_current_locale
+}
+
+
+{
+
+ Memcheck:Cond
+ fun:inflate
+ fun:ZIPDecode
+ fun:_TIFFReadEncodedTileAndAllocBuffer
+ ...
+}
diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py
index a243c0260..629e9ac00 100644
--- a/Tests/oss-fuzz/test_fuzzers.py
+++ b/Tests/oss-fuzz/test_fuzzers.py
@@ -2,12 +2,19 @@ import subprocess
import sys
import fuzzers
+import packaging
import pytest
-from PIL import Image
+from PIL import Image, features
if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True)
+if features.check("libjpeg_turbo"):
+ version = packaging.version.parse(features.version("libjpeg_turbo"))
+ if version.major == 2 and version.minor == 0:
+ pytestmark = pytest.mark.valgrind_known_error(
+ reason="Known failing with libjpeg_turbo 2.0"
+ )
@pytest.mark.parametrize(
@@ -37,6 +44,8 @@ def test_fuzz_images(path):
):
# Known Image.* exceptions
assert True
+ finally:
+ fuzzers.disable_decompressionbomb_error()
@pytest.mark.parametrize(
diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py
index 80ab92666..d918ef941 100644
--- a/Tests/test_decompression_bomb.py
+++ b/Tests/test_decompression_bomb.py
@@ -10,8 +10,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb:
- @classmethod
- def teardown_class(cls):
+ def teardown_method(self, method):
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self):
@@ -52,6 +51,7 @@ class TestDecompressionBomb:
with Image.open(TEST_FILE):
pass
+ @pytest.mark.xfail(reason="different exception")
def test_exception_ico(self):
with pytest.raises(Image.DecompressionBombError):
with Image.open("Tests/images/decompression_bomb.ico"):
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 97e2a150e..d48e5ce07 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -249,8 +249,8 @@ def test_apng_mode():
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
- assert im.getpixel((0, 0)) == (0, 255, 0, 255)
- assert im.getpixel((64, 32)) == (0, 255, 0, 255)
+ assert im.getpixel((0, 0)) == (255, 0, 0, 0)
+ assert im.getpixel((64, 32)) == (255, 0, 0, 0)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert im.mode == "P"
@@ -312,7 +312,7 @@ def test_apng_syntax_errors():
exception = e
assert exception is None
- with pytest.raises(SyntaxError):
+ with pytest.raises(OSError):
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
im.seek(im.n_frames - 1)
im.load()
@@ -433,12 +433,20 @@ def test_apng_save_duration_loop(tmp_path):
# test removal of duplicated frames
frame = Image.new("RGBA", (128, 64), (255, 0, 0, 255))
- frame.save(test_file, save_all=True, append_images=[frame], duration=[500, 250])
+ frame.save(
+ test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
+ )
with Image.open(test_file) as im:
im.load()
assert im.n_frames == 1
assert im.info.get("duration") == 750
+ # test info duration
+ frame.info["duration"] = 750
+ frame.save(test_file, save_all=True)
+ with Image.open(test_file) as im:
+ assert im.info.get("duration") == 750
+
def test_apng_save_disposal(tmp_path):
test_file = str(tmp_path / "temp.png")
@@ -529,6 +537,17 @@ def test_apng_save_disposal(tmp_path):
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
+ # test info disposal
+ red.info["disposal"] = PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND
+ red.save(
+ test_file,
+ save_all=True,
+ append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))],
+ )
+ with Image.open(test_file) as im:
+ im.seek(1)
+ assert im.getpixel((64, 32)) == (0, 0, 0, 0)
+
def test_apng_save_disposal_previous(tmp_path):
test_file = str(tmp_path / "temp.png")
@@ -609,3 +628,10 @@ def test_apng_save_blend(tmp_path):
im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
+
+ # test info blend
+ red.info["blend"] = PngImagePlugin.APNG_BLEND_OP_OVER
+ red.save(test_file, save_all=True, append_images=[green, transparent])
+ with Image.open(test_file) as im:
+ im.seek(2)
+ assert im.getpixel((0, 0)) == (0, 255, 0, 255)
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index 864607301..15bd7e4f8 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image
from .helper import assert_image_equal_tofile
@@ -16,3 +18,22 @@ def test_load_blp2_dxt1():
def test_load_blp2_dxt1a():
with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
+
+
+@pytest.mark.parametrize(
+ "test_file",
+ [
+ "Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp",
+ "Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp",
+ "Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp",
+ "Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp",
+ "Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp",
+ "Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp",
+ "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
+ ],
+)
+def test_crashes(test_file):
+ with open(test_file, "rb") as f:
+ with Image.open(f) as im:
+ with pytest.raises(OSError):
+ im.load()
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index d5fe2a4dd..47fc97df0 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -63,7 +63,7 @@ def test_dpi():
output.seek(0)
with Image.open(output) as reloaded:
- assert reloaded.info["dpi"] == dpi
+ assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161)
def test_save_bmp_with_dpi(tmp_path):
@@ -71,6 +71,7 @@ def test_save_bmp_with_dpi(tmp_path):
# Arrange
outfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.bmp") as im:
+ assert im.info["dpi"] == (95.98654816726399, 95.98654816726399)
# Act
im.save(outfile, "JPEG", dpi=im.info["dpi"])
@@ -78,31 +79,17 @@ def test_save_bmp_with_dpi(tmp_path):
# Assert
with Image.open(outfile) as reloaded:
reloaded.load()
- assert im.info["dpi"] == reloaded.info["dpi"]
- assert im.size == reloaded.size
+ assert reloaded.info["dpi"] == (96, 96)
+ assert reloaded.size == im.size
assert reloaded.format == "JPEG"
-def test_load_dpi_rounding():
- # Round up
- with Image.open("Tests/images/hopper.bmp") as im:
- assert im.info["dpi"] == (96, 96)
-
- # Round down
- with Image.open("Tests/images/hopper_roundDown.bmp") as im:
- assert im.info["dpi"] == (72, 72)
-
-
-def test_save_dpi_rounding(tmp_path):
+def test_save_float_dpi(tmp_path):
outfile = str(tmp_path / "temp.bmp")
with Image.open("Tests/images/hopper.bmp") as im:
- im.save(outfile, dpi=(72.2, 72.2))
+ im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
with Image.open(outfile) as reloaded:
- assert reloaded.info["dpi"] == (72, 72)
-
- im.save(outfile, dpi=(72.8, 72.8))
- with Image.open(outfile) as reloaded:
- assert reloaded.info["dpi"] == (73, 73)
+ assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306)
def test_load_dib():
@@ -136,3 +123,10 @@ def test_rgba_bitfields():
im = Image.merge("RGB", (r, g, b))
assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
+
+
+def test_offset():
+ # This image has been hexedited
+ # to exclude the palette size from the pixel data offset
+ with Image.open("Tests/images/pal8_offset.bmp") as im:
+ assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index 682cd048b..2f46ed77e 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -5,16 +5,21 @@ import pytest
from PIL import DdsImagePlugin, Image
-from .helper import assert_image_equal, assert_image_equal_tofile
+from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
+TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
+TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
+TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
+TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds"
TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds"
TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds"
-TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds"
+TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
+TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
def test_sanity_dxt1():
@@ -31,6 +36,19 @@ def test_sanity_dxt1():
assert_image_equal(im, target)
+def test_sanity_dxt3():
+ """Check DXT3 images can be opened"""
+
+ with Image.open(TEST_FILE_DXT3) as im:
+ im.load()
+
+ assert im.format == "DDS"
+ assert im.mode == "RGBA"
+ assert im.size == (256, 256)
+
+ assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))
+
+
def test_sanity_dxt5():
"""Check DXT5 images can be opened"""
@@ -44,17 +62,28 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
-def test_sanity_dxt3():
- """Check DXT3 images can be opened"""
+@pytest.mark.parametrize(
+ ("image_path", "expected_path"),
+ (
+ # hexeditted to be typeless
+ (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM),
+ (TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM),
+ # hexeditted to use DX10 FourCC
+ (TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S),
+ (TEST_FILE_BC5S, TEST_FILE_BC5S),
+ ),
+)
+def test_dx10_bc5(image_path, expected_path):
+ """Check DX10 BC5 images can be opened"""
- with Image.open(TEST_FILE_DXT3) as im:
+ with Image.open(image_path) as im:
im.load()
assert im.format == "DDS"
- assert im.mode == "RGBA"
+ assert im.mode == "RGB"
assert im.size == (256, 256)
- assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))
+ assert_image_equal_tofile(im, expected_path.replace(".dds", ".png"))
def test_dx10_bc7():
@@ -124,44 +153,51 @@ def test_unimplemented_dxgi_format():
def test_uncompressed_rgb():
"""Check uncompressed RGB images can be opened"""
+ # convert -format dds -define dds:compression=none hopper.jpg hopper.dds
with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im:
- im.load()
+ assert im.format == "DDS"
+ assert im.mode == "RGB"
+ assert im.size == (128, 128)
+ assert_image_equal_tofile(im, "Tests/images/hopper.png")
+
+ # Test image with alpha
+ with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im:
assert im.format == "DDS"
assert im.mode == "RGBA"
assert im.size == (800, 600)
assert_image_equal_tofile(
- im, TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png")
+ im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png")
)
-def test__validate_true():
+def test__accept_true():
"""Check valid prefix"""
# Arrange
prefix = b"DDS etc"
# Act
- output = DdsImagePlugin._validate(prefix)
+ output = DdsImagePlugin._accept(prefix)
# Assert
assert output
-def test__validate_false():
+def test__accept_false():
"""Check invalid prefix"""
# Arrange
prefix = b"something invalid"
# Act
- output = DdsImagePlugin._validate(prefix)
+ output = DdsImagePlugin._accept(prefix)
# Assert
assert not output
def test_short_header():
- """ Check a short header"""
+ """Check a short header"""
with open(TEST_FILE_DXT5, "rb") as f:
img_file = f.read()
@@ -174,7 +210,7 @@ def test_short_header():
def test_short_file():
- """ Check that the appropriate error is thrown for a short file"""
+ """Check that the appropriate error is thrown for a short file"""
with open(TEST_FILE_DXT5, "rb") as f:
img_file = f.read()
@@ -187,7 +223,46 @@ def test_short_file():
short_file()
+def test_dxt5_colorblock_alpha_issue_4142():
+ """Check that colorblocks are decoded correctly in DXT5"""
+
+ with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
+ px = im.getpixel((0, 0))
+ assert px[0] != 0
+ assert px[1] != 0
+ assert px[2] != 0
+
+ px = im.getpixel((1, 0))
+ assert px[0] != 0
+ assert px[1] != 0
+ assert px[2] != 0
+
+
def test_unimplemented_pixel_format():
with pytest.raises(NotImplementedError):
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
pass
+
+
+def test_save_unsupported_mode(tmp_path):
+ out = str(tmp_path / "temp.dds")
+ im = hopper("HSV")
+ with pytest.raises(OSError):
+ im.save(out)
+
+
+@pytest.mark.parametrize(
+ ("mode", "test_file"),
+ [
+ ("RGB", "Tests/images/hopper.png"),
+ ("RGBA", "Tests/images/pil123rgba.png"),
+ ],
+)
+def test_save(mode, test_file, tmp_path):
+ out = str(tmp_path / "temp.dds")
+ with Image.open(test_file) as im:
+ assert im.mode == mode
+ im.save(out)
+
+ with Image.open(out) as reloaded:
+ assert_image_equal(im, reloaded)
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index ea91375da..4c0b96f73 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -8,6 +8,7 @@ from .helper import (
assert_image_similar,
assert_image_similar_tofile,
hopper,
+ mark_if_feature_version,
skip_unless_feature,
)
@@ -64,7 +65,9 @@ def test_invalid_file():
EpsImagePlugin.EpsImageFile(invalid_file)
-@pytest.mark.valgrind_known_error(reason="Known Failing")
+@mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_cmyk():
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
@@ -93,6 +96,17 @@ def test_showpage():
assert_image_similar(plot_image, target, 6)
+@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
+def test_transparency():
+ with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
+ plot_image.load(transparency=True)
+ assert plot_image.mode == "RGBA"
+
+ with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
+ # fonts could be slightly different
+ assert_image_similar(plot_image, target, 6)
+
+
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_file_object(tmp_path):
# issue 479
@@ -264,3 +278,15 @@ def test_emptyline():
assert image.mode == "RGB"
assert image.size == (460, 352)
assert image.format == "EPS"
+
+
+@pytest.mark.timeout(timeout=5)
+@pytest.mark.parametrize(
+ "test_file",
+ ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
+)
+def test_timeout(test_file):
+ with open(test_file, "rb") as f:
+ with pytest.raises(Image.UnidentifiedImageError):
+ with Image.open(f):
+ pass
diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py
index 6dc7c4602..c77457947 100644
--- a/Tests/test_file_fitsstub.py
+++ b/Tests/test_file_fitsstub.py
@@ -1,3 +1,5 @@
+from io import BytesIO
+
import pytest
from PIL import FitsStubImagePlugin, Image
@@ -11,10 +13,8 @@ def test_open():
# Assert
assert im.format == "FITS"
-
- # Dummy data from the stub
- assert im.mode == "F"
- assert im.size == (1, 1)
+ assert im.size == (128, 128)
+ assert im.mode == "L"
def test_invalid_file():
@@ -35,6 +35,21 @@ def test_load():
im.load()
+def test_truncated_fits():
+ # No END to headers
+ image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE"
+ with pytest.raises(OSError):
+ FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data))
+
+
+def test_naxis_zero():
+ # This test image has been manually hexedited
+ # to set the number of data axes to zero
+ with pytest.raises(ValueError):
+ with Image.open("Tests/images/hopper_naxis_zero.fits"):
+ pass
+
+
def test_save():
# Arrange
with Image.open(TEST_FILE) as im:
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index 0d9748a95..675e06bf8 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -123,3 +123,31 @@ def test_seek():
im.seek(50)
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
+
+
+@pytest.mark.parametrize(
+ "test_file",
+ [
+ "Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli",
+ "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli",
+ ],
+)
+@pytest.mark.timeout(timeout=3)
+def test_timeouts(test_file):
+ with open(test_file, "rb") as f:
+ with Image.open(f) as im:
+ with pytest.raises(OSError):
+ im.load()
+
+
+@pytest.mark.parametrize(
+ "test_file",
+ [
+ "Tests/images/crash-5762152299364352.fli",
+ ],
+)
+def test_crash(test_file):
+ with open(test_file, "rb") as f:
+ with Image.open(f) as im:
+ with pytest.raises(OSError):
+ im.load()
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 1b2314d51..00bf582fa 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -163,6 +163,32 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5
+@pytest.mark.parametrize(
+ "path, mode",
+ (
+ ("Tests/images/dispose_bgnd.gif", "RGB"),
+ # Hexeditted copy of dispose_bgnd to add transparency
+ ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
+ ),
+)
+def test_loading_multiple_palettes(path, mode):
+ with Image.open(path) as im:
+ assert im.mode == "P"
+ first_frame_colors = im.palette.colors.keys()
+ original_color = im.convert("RGB").load()[0, 0]
+
+ im.seek(1)
+ assert im.mode == mode
+ if mode == "RGBA":
+ im = im.convert("RGB")
+
+ # Check a color only from the old palette
+ assert im.load()[0, 0] == original_color
+
+ # Check a color from the new palette
+ assert im.load()[24, 24] not in first_frame_colors
+
+
def test_headers_saving_for_animated_gifs(tmp_path):
important_headers = ["background", "version", "duration", "loop"]
# Multiframe image
@@ -298,6 +324,12 @@ def test_eoferror():
im.seek(n_frames - 1)
+def test_first_frame_transparency():
+ with Image.open("Tests/images/first_frame_transparency.gif") as im:
+ px = im.load()
+ assert px[0, 0] == im.info["transparency"]
+
+
def test_dispose_none():
with Image.open("Tests/images/dispose_none.gif") as img:
try:
@@ -318,7 +350,7 @@ def test_dispose_none_load_end():
with Image.open("Tests/images/dispose_none_load_end.gif") as img:
img.seek(1)
- assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.gif")
+ assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
def test_dispose_background():
@@ -331,6 +363,27 @@ def test_dispose_background():
pass
+def test_dispose_background_transparency():
+ with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
+ img.seek(2)
+ px = img.load()
+ assert px[35, 30][3] == 0
+
+
+def test_transparent_dispose():
+ expected_colors = [
+ (2, 1, 2),
+ ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
+ ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
+ ]
+ with Image.open("Tests/images/transparent_dispose.gif") as img:
+ for frame in range(3):
+ img.seek(frame)
+ for x in range(3):
+ color = img.getpixel((x, 0))
+ assert color == expected_colors[frame][x]
+
+
def test_dispose_previous():
with Image.open("Tests/images/dispose_prev.gif") as img:
try:
@@ -341,6 +394,25 @@ def test_dispose_previous():
pass
+def test_dispose_previous_first_frame():
+ with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
+ im.seek(1)
+ assert_image_equal_tofile(
+ im, "Tests/images/dispose_prev_first_frame_seeked.png"
+ )
+
+
+def test_previous_frame_loaded():
+ with Image.open("Tests/images/dispose_none.gif") as img:
+ img.load()
+ img.seek(1)
+ img.load()
+ img.seek(2)
+ with Image.open("Tests/images/dispose_none.gif") as img_skipped:
+ img_skipped.seek(2)
+ assert_image_equal(img_skipped, img)
+
+
def test_save_dispose(tmp_path):
out = str(tmp_path / "temp.gif")
im_list = [
@@ -373,14 +445,15 @@ def test_save_dispose(tmp_path):
def test_dispose2_palette(tmp_path):
out = str(tmp_path / "temp.gif")
- # 4 backgrounds: White, Grey, Black, Red
+ # Four colors: white, grey, black, red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
im_list = []
for circle in circles:
+ # Red background
img = Image.new("RGB", (100, 100), (255, 0, 0))
- # Red circle in center of each frame
+ # Circle in center of each frame
d = ImageDraw.Draw(img)
d.ellipse([(40, 40), (60, 60)], fill=circle)
@@ -465,15 +538,28 @@ def test_dispose2_background(tmp_path):
with Image.open(out) as im:
im.seek(1)
- assert im.getpixel((0, 0)) == 0
+ assert im.getpixel((0, 0)) == (255, 0, 0)
-def test_iss634():
+def test_transparency_in_second_frame():
+ with Image.open("Tests/images/different_transparency.gif") as im:
+ assert im.info["transparency"] == 0
+
+ # Seek to the second frame
+ im.seek(im.tell() + 1)
+ assert "transparency" not in im.info
+
+ assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
+
+
+def test_no_transparency_in_second_frame():
with Image.open("Tests/images/iss634.gif") as img:
# Seek to the second frame
img.seek(img.tell() + 1)
+ assert "transparency" not in img.info
+
# All transparent pixels should be replaced with the color from the first frame
- assert img.histogram()[img.info["transparency"]] == 0
+ assert img.histogram()[255] == 0
def test_duration(tmp_path):
@@ -717,10 +803,10 @@ def test_rgb_transparency(tmp_path):
# Single frame
im = Image.new("RGB", (1, 1))
im.info["transparency"] = (255, 0, 0)
- pytest.warns(UserWarning, im.save, out)
+ im.save(out)
with Image.open(out) as reloaded:
- assert "transparency" not in reloaded.info
+ assert "transparency" in reloaded.info
# Multiple frames
im = Image.new("RGB", (1, 1))
@@ -762,7 +848,7 @@ def test_palette_save_P(tmp_path):
# Forcing a non-straight grayscale palette.
im = hopper("P")
- palette = bytes([255 - i // 3 for i in range(768)])
+ palette = bytes(255 - i // 3 for i in range(768))
out = str(tmp_path / "temp.gif")
im.save(out, palette=palette)
@@ -772,6 +858,29 @@ def test_palette_save_P(tmp_path):
assert_image_equal(reloaded, im)
+def test_palette_save_all_P(tmp_path):
+ frames = []
+ colors = ((255, 0, 0), (0, 255, 0))
+ for color in colors:
+ frame = Image.new("P", (100, 100))
+ frame.putpalette(color)
+ frames.append(frame)
+
+ out = str(tmp_path / "temp.gif")
+ frames[0].save(
+ out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
+ )
+
+ with Image.open(out) as im:
+ # Assert that the frames are correct, and each frame has the same palette
+ assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
+ assert im.palette.palette == im.global_palette.palette
+
+ im.seek(1)
+ assert_image_equal(im.convert("RGB"), frames[1].convert("RGB"))
+ assert im.palette.palette == im.global_palette.palette
+
+
def test_palette_save_ImagePalette(tmp_path):
# Pass in a different palette, as an ImagePalette.ImagePalette
# effectively the same as test_palette_save_P
@@ -784,7 +893,7 @@ def test_palette_save_ImagePalette(tmp_path):
with Image.open(out) as reloaded:
im.putpalette(palette)
- assert_image_equal(reloaded, im)
+ assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
def test_save_I(tmp_path):
@@ -806,7 +915,7 @@ def test_getdata():
im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0}
- passed_palette = bytes([255 - i // 3 for i in range(768)])
+ passed_palette = bytes(255 - i // 3 for i in range(768))
GifImagePlugin._FORCE_OPTIMIZE = True
try:
@@ -840,3 +949,21 @@ def test_extents():
assert im.size == (100, 100)
im.seek(1)
assert im.size == (150, 150)
+
+
+def test_missing_background():
+ # The Global Color Table Flag isn't set, so there is no background color index,
+ # but the disposal method is "Restore to background color"
+ with Image.open("Tests/images/missing_background.gif") as im:
+ im.seek(1)
+ assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")
+
+
+def test_saving_rgba(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ with Image.open("Tests/images/transparent.png") as im:
+ im.save(out)
+
+ with Image.open(out) as reloaded:
+ reloaded_rgba = reloaded.convert("RGBA")
+ assert reloaded_rgba.load()[0, 0][3] == 0
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
index 30ec3dc72..3afbbeaac 100644
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -1,9 +1,9 @@
import io
-import sys
+import os
import pytest
-from PIL import IcnsImagePlugin, Image, features
+from PIL import IcnsImagePlugin, Image, _binary, features
from .helper import assert_image_equal, assert_image_similar_tofile
@@ -28,7 +28,6 @@ def test_sanity():
assert im.format == "ICNS"
-@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save(tmp_path):
temp_file = str(tmp_path / "temp.icns")
@@ -40,8 +39,12 @@ def test_save(tmp_path):
assert reread.size == (1024, 1024)
assert reread.format == "ICNS"
+ file_length = os.path.getsize(temp_file)
+ with open(temp_file, "rb") as fp:
+ fp.seek(4)
+ assert _binary.i32be(fp.read(4)) == file_length
+
-@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_append_images(tmp_path):
temp_file = str(tmp_path / "temp.icns")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
@@ -57,7 +60,6 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im)
-@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_fp():
fp = io.BytesIO()
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 5ace0c55e..317264db6 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -18,6 +18,17 @@ def test_sanity():
assert im.get_format_mimetype() == "image/x-icon"
+def test_mask():
+ with Image.open("Tests/images/hopper_mask.ico") as im:
+ assert_image_equal_tofile(im, "Tests/images/hopper_mask.png")
+
+
+def test_black_and_white():
+ with Image.open("Tests/images/black_and_white.ico") as im:
+ assert im.mode == "RGBA"
+ assert im.size == (16, 16)
+
+
def test_invalid_file():
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
@@ -50,6 +61,35 @@ def test_save_to_bytes():
assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS))
+@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
+def test_save_to_bytes_bmp(mode):
+ output = io.BytesIO()
+ im = hopper(mode)
+ im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
+
+ # The default image
+ output.seek(0)
+ with Image.open(output) as reloaded:
+ assert reloaded.info["sizes"] == {(32, 32), (64, 64)}
+
+ assert "RGBA" == reloaded.mode
+ assert (64, 64) == reloaded.size
+ assert reloaded.format == "ICO"
+ im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA")
+ assert_image_equal(reloaded, im)
+
+ # The other one
+ output.seek(0)
+ with Image.open(output) as reloaded:
+ reloaded.size = (32, 32)
+
+ assert "RGBA" == reloaded.mode
+ assert (32, 32) == reloaded.size
+ assert reloaded.format == "ICO"
+ im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA")
+ assert_image_equal(reloaded, im)
+
+
def test_incorrect_size():
with Image.open(TEST_ICO_FILE) as im:
with pytest.raises(ValueError):
@@ -119,5 +159,4 @@ def test_draw_reloaded(tmp_path):
im.save(outfile)
with Image.open(outfile) as im:
- im.save("Tests/images/hopper_draw.ico")
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 3ee33d65f..4b2ffe70d 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -24,9 +24,15 @@ from .helper import (
djpeg_available,
hopper,
is_win32,
+ mark_if_feature_version,
skip_unless_feature,
)
+try:
+ import defusedxml.ElementTree as ElementTree
+except ImportError:
+ ElementTree = None
+
TEST_FILE = "Tests/images/hopper.jpg"
@@ -79,26 +85,26 @@ class TestFileJpeg:
f = "Tests/images/pil_sample_cmyk.jpg"
with Image.open(f) as im:
# the source image has red pixels in the upper left corner.
- c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))]
+ c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
assert c == 0.0
assert m > 0.8
assert y > 0.8
assert k == 0.0
# the opposite corner is black
- c, m, y, k = [
+ c, m, y, k = (
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
- ]
+ )
assert k > 0.9
# roundtrip, and check again
im = self.roundtrip(im)
- c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))]
+ c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
assert c == 0.0
assert m > 0.8
assert y > 0.8
assert k == 0.0
- c, m, y, k = [
+ c, m, y, k = (
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
- ]
+ )
assert k > 0.9
@pytest.mark.parametrize(
@@ -116,7 +122,9 @@ class TestFileJpeg:
assert test(100, 200) == (100, 200)
assert test(0) is None # square pixels
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_icc(self, tmp_path):
# Test ICC support
with Image.open("Tests/images/rgb.jpg") as im1:
@@ -156,7 +164,9 @@ class TestFileJpeg:
test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte
test(ImageFile.MAXBLOCK * 4 + 3) # large block
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_large_icc_meta(self, tmp_path):
# https://github.com/python-pillow/Pillow/issues/148
# Sometimes the meta data on the icc_profile block is bigger than
@@ -423,7 +433,9 @@ class TestFileJpeg:
with Image.open(filename):
pass
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_truncated_jpeg_should_read_all_the_data(self):
filename = "Tests/images/truncated_jpeg.jpg"
ImageFile.LOAD_TRUNCATED_IMAGES = True
@@ -442,7 +454,9 @@ class TestFileJpeg:
with pytest.raises(OSError):
im.load()
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_qtables(self, tmp_path):
def _n_qtables_helper(n, test_file):
with Image.open(test_file) as im:
@@ -452,7 +466,7 @@ class TestFileJpeg:
assert len(im.quantization) == n
reloaded = self.roundtrip(im, qtables="keep")
assert im.quantization == reloaded.quantization
- assert reloaded.quantization[0].typecode == "B"
+ assert max(reloaded.quantization[0]) <= 255
with Image.open("Tests/images/hopper.jpg") as im:
qtables = im.quantization
@@ -464,7 +478,8 @@ class TestFileJpeg:
# valid bounds for baseline qtable
bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)]
- self.roundtrip(im, qtables=[bounds_qtable])
+ im2 = self.roundtrip(im, qtables=[bounds_qtable])
+ assert im2.quantization == {0: bounds_qtable}
# values from wizard.txt in jpeg9-a src package.
standard_l_qtable = [
@@ -575,6 +590,12 @@ class TestFileJpeg:
assert max(im2.quantization[0]) <= 255
assert max(im2.quantization[1]) <= 255
+ def test_convert_dict_qtables_deprecation(self):
+ with pytest.warns(DeprecationWarning):
+ qtable = {0: [1, 2, 3, 4]}
+ qtable2 = JpegImagePlugin.convert_dict_qtables(qtable)
+ assert qtable == qtable2
+
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
def test_load_djpeg(self):
with Image.open(TEST_FILE) as img:
@@ -609,7 +630,7 @@ class TestFileJpeg:
reloaded.save(f, quality="keep", optimize=True)
def test_bad_mpo_header(self):
- """ Treat unknown MPO as JPEG """
+ """Treat unknown MPO as JPEG"""
# Arrange
# Act
@@ -647,15 +668,6 @@ class TestFileJpeg:
reloaded.load()
assert im.info["dpi"] == reloaded.info["dpi"]
- def test_load_dpi_rounding(self):
- # Round up
- with Image.open("Tests/images/iptc_roundUp.jpg") as im:
- assert im.info["dpi"] == (44, 44)
-
- # Round down
- with Image.open("Tests/images/iptc_roundDown.jpg") as im:
- assert im.info["dpi"] == (2, 2)
-
def test_save_dpi_rounding(self, tmp_path):
outfile = str(tmp_path / "temp.jpg")
with Image.open("Tests/images/hopper.jpg") as im:
@@ -706,6 +718,15 @@ class TestFileJpeg:
# This should return the default, and not raise a ZeroDivisionError
assert im.info.get("dpi") == (72, 72)
+ def test_dpi_exif_string(self):
+ # Arrange
+ # 0x011A tag in this exif contains string '300300\x02'
+ with Image.open("Tests/images/broken_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):
# Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
@@ -726,7 +747,9 @@ class TestFileJpeg:
# OSError for unidentified image.
assert im.info.get("dpi") == (72, 72)
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_exif_x_resolution(self, tmp_path):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
@@ -757,7 +780,9 @@ class TestFileJpeg:
# Act / Assert
assert im._getexif()[306] == "2017:03:13 23:03:09"
- @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_photoshop(self):
with Image.open("Tests/images/photoshop-200dpi.jpg") as im:
assert im.info["photoshop"][0x03ED] == {
@@ -782,6 +807,20 @@ class TestFileJpeg:
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
assert [65504, 24] == apps_13_lengths
+ def test_adobe_transform(self):
+ with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
+ assert im.info["adobe_transform"] == 1
+
+ with Image.open("Tests/images/pil_sample_cmyk.jpg") as im:
+ assert im.info["adobe_transform"] == 2
+
+ # This image has been manually hexedited
+ # so that the APP14 reports its length to be 11,
+ # leaving no room for "adobe_transform"
+ with Image.open("Tests/images/truncated_app14.jpg") as im:
+ assert "adobe" in im.info
+ assert "adobe_transform" not in im.info
+
def test_icc_after_SOF(self):
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile"
@@ -805,6 +844,56 @@ class TestFileJpeg:
# Assert the entire file has not been read
assert 0 < buffer.max_pos < size
+ def test_getxmp(self):
+ with Image.open("Tests/images/xmp_test.jpg") as im:
+ if ElementTree is None:
+ with pytest.warns(UserWarning):
+ assert im.getxmp() == {}
+ else:
+ xmp = im.getxmp()
+
+ description = xmp["xmpmeta"]["RDF"]["Description"]
+ assert description["DerivedFrom"] == {
+ "documentID": "8367D410E636EA95B7DE7EBA1C43A412",
+ "originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412",
+ }
+ assert description["Look"]["Description"]["Group"]["Alt"]["li"] == {
+ "lang": "x-default",
+ "text": "Profiles",
+ }
+ assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"]
+
+ # Attribute
+ assert description["Version"] == "10.4"
+
+ if ElementTree is not None:
+ with Image.open("Tests/images/hopper.jpg") as im:
+ assert im.getxmp() == {}
+
+ @pytest.mark.timeout(timeout=1)
+ def test_eof(self):
+ # Even though this decoder never says that it is finished
+ # the image should still end when there is no new data
+ class InfiniteMockPyDecoder(ImageFile.PyDecoder):
+ def decode(self, buffer):
+ return 0, 0
+
+ decoder = InfiniteMockPyDecoder(None)
+
+ def closure(mode, *args):
+ decoder.__init__(mode, *args)
+ return decoder
+
+ Image.register_decoder("INFINITE", closure)
+
+ with Image.open(TEST_FILE) as im:
+ im.tile = [
+ ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
+ ]
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+ im.load()
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
+
@pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg")
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 13ae09af5..ca410162a 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -1,18 +1,20 @@
+import os
import re
from io import BytesIO
import pytest
-from PIL import Image, ImageFile, Jpeg2KImagePlugin, features
+from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
from .helper import (
assert_image_equal,
assert_image_similar,
assert_image_similar_tofile,
- is_big_endian,
skip_unless_feature,
)
+EXTRA_DIR = "Tests/images/jpeg2000"
+
pytestmark = skip_unless_feature("jpg_2000")
test_card = Image.open("Tests/images/test-card.png")
@@ -28,9 +30,9 @@ def roundtrip(im, **options):
im.save(out, "JPEG2000", **options)
test_bytes = out.tell()
out.seek(0)
- im = Image.open(out)
- im.bytes = test_bytes # for testing only
- im.load()
+ with Image.open(out) as im:
+ im.bytes = test_bytes # for testing only
+ im.load()
return im
@@ -124,6 +126,16 @@ def test_prog_res_rt():
assert_image_equal(im, test_card)
+def test_default_num_resolutions():
+ for num_resolutions in range(2, 6):
+ d = 1 << (num_resolutions - 1)
+ im = test_card.resize((d - 1, d - 1))
+ with pytest.raises(OSError):
+ roundtrip(im, num_resolutions=num_resolutions)
+ reloaded = roundtrip(im)
+ assert_image_equal(im, reloaded)
+
+
def test_reduce():
with Image.open("Tests/images/test-card-lossless.jp2") as im:
assert callable(im.reduce)
@@ -138,6 +150,38 @@ def test_reduce():
assert im.size == (40, 30)
+def test_load_dpi():
+ with Image.open("Tests/images/test-card-lossless.jp2") as im:
+ assert im.info["dpi"] == (71.9836, 71.9836)
+
+ with Image.open("Tests/images/zero_dpi.jp2") as im:
+ assert "dpi" not in im.info
+
+
+def test_restricted_icc_profile():
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+ try:
+ # JPEG2000 image with a restricted ICC profile and a known colorspace
+ with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
+ assert im.mode == "RGB"
+ finally:
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
+
+
+def test_header_errors():
+ for path in (
+ "Tests/images/invalid_header_length.jp2",
+ "Tests/images/not_enough_data.jp2",
+ ):
+ with pytest.raises(UnidentifiedImageError):
+ with Image.open(path):
+ pass
+
+ with pytest.raises(OSError):
+ with Image.open("Tests/images/expected_to_read.jp2"):
+ pass
+
+
def test_layers_type(tmp_path):
outfile = str(tmp_path / "temp_layers.jp2")
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
@@ -189,13 +233,11 @@ def test_16bit_monochrome_has_correct_mode():
assert jp2.mode == "I;16"
-@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_16bit_monochrome_jp2_like_tiff():
with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit:
assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3)
-@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_16bit_monochrome_j2k_like_tiff():
with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit:
assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3)
@@ -231,3 +273,42 @@ def test_parser_feed():
# Assert
assert p.image.size == (640, 480)
+
+
+@pytest.mark.skipif(
+ not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
+)
+@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2"))
+def test_subsampling_decode(name):
+ test = f"{EXTRA_DIR}/{name}.jp2"
+ reference = f"{EXTRA_DIR}/{name}.ppm"
+
+ with Image.open(test) as im:
+ epsilon = 3 # for YCbCr images
+ with Image.open(reference) as im2:
+ width, height = im2.size
+ if name[-1] == "2":
+ # RGB reference images are downscaled
+ epsilon = 3e-3
+ width, height = width * 2, height * 2
+ expected = im2.resize((width, height), Image.NEAREST)
+ assert_image_similar(im, expected, epsilon)
+
+
+@pytest.mark.parametrize(
+ "test_file",
+ [
+ "Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k",
+ "Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k",
+ "Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k",
+ "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k",
+ ],
+)
+def test_crashes(test_file):
+ with open(test_file, "rb") as f:
+ with Image.open(f) as im:
+ # Valgrind should not complain here
+ try:
+ im.load()
+ except OSError:
+ pass
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 22b641b5f..e40a19394 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -9,7 +9,7 @@ from ctypes import c_float
import pytest
from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features
-from PIL.TiffImagePlugin import SUBIFD
+from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD
from .helper import (
assert_image_equal,
@@ -17,6 +17,7 @@ from .helper import (
assert_image_similar,
assert_image_similar_tofile,
hopper,
+ mark_if_feature_version,
skip_unless_feature,
)
@@ -96,13 +97,13 @@ class TestFileLibTiff(LibTiffTestCase):
self._assert_noerr(tmp_path, im)
def test_g4_eq_png(self):
- """ Checking that we're actually getting the data that we expect"""
+ """Checking that we're actually getting the data that we expect"""
with Image.open("Tests/images/hopper_bw_500.png") as png:
assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif")
# see https://github.com/python-pillow/Pillow/issues/279
def test_g4_fillorder_eq_png(self):
- """ Checking that we're actually getting the data that we expect"""
+ """Checking that we're actually getting the data that we expect"""
with Image.open("Tests/images/g4-fillorder-test.tif") as g4:
assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png")
@@ -136,7 +137,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
def test_write_metadata(self, tmp_path):
- """ Test metadata writing through libtiff """
+ """Test metadata writing through libtiff"""
for legacy_api in [False, True]:
f = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper_g4.tif") as img:
@@ -577,6 +578,17 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False
+ def test_multipage_seek_backwards(self):
+ TiffImagePlugin.READ_LIBTIFF = True
+ with Image.open("Tests/images/multipage.tiff") as im:
+ im.seek(1)
+ im.load()
+
+ im.seek(0)
+ assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
+
+ TiffImagePlugin.READ_LIBTIFF = False
+
def test__next(self):
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/hopper.tif") as im:
@@ -658,6 +670,15 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_LIBTIFF = False
+ def test_save_ycbcr(self, tmp_path):
+ im = hopper("YCbCr")
+ outfile = str(tmp_path / "temp.tif")
+ im.save(outfile, compression="jpeg")
+
+ with Image.open(outfile) as reloaded:
+ assert reloaded.tag_v2[530] == (1, 1)
+ assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
+
def test_crashing_metadata(self, tmp_path):
# issue 1597
with Image.open("Tests/images/rdf.tif") as im:
@@ -804,6 +825,17 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
+ def test_sampleformat_write(self, tmp_path):
+ im = Image.new("F", (1, 1))
+ out = str(tmp_path / "temp.tif")
+ TiffImagePlugin.WRITE_LIBTIFF = True
+ im.save(out)
+ TiffImagePlugin.WRITE_LIBTIFF = False
+
+ with Image.open(out) as reloaded:
+ assert reloaded.mode == "F"
+ assert reloaded.getexif()[SAMPLEFORMAT] == 3
+
def test_lzw(self):
with Image.open("Tests/images/hopper_lzw.tif") as im:
assert im.mode == "RGB"
@@ -822,13 +854,17 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5)
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_strip_ycbcr_jpeg_2x2_sampling(self):
infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5)
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_strip_ycbcr_jpeg_1x1_sampling(self):
infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im:
@@ -839,13 +875,17 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(infile) as im:
assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5)
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_tiled_ycbcr_jpeg_1x1_sampling(self):
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif"
with Image.open(infile) as im:
assert_image_equal_tofile(im, "Tests/images/flower2.jpg")
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_tiled_ycbcr_jpeg_2x2_sampling(self):
infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif"
with Image.open(infile) as im:
@@ -891,9 +931,31 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im:
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
+ @pytest.mark.parametrize("compression", (None, "jpeg"))
+ def test_block_tile_tags(self, compression, tmp_path):
+ im = hopper()
+ out = str(tmp_path / "temp.tif")
+
+ tags = {
+ TiffImagePlugin.TILEWIDTH: 256,
+ TiffImagePlugin.TILELENGTH: 256,
+ TiffImagePlugin.TILEOFFSETS: 256,
+ TiffImagePlugin.TILEBYTECOUNTS: 256,
+ }
+ im.save(out, exif=tags, compression=compression)
+
+ with Image.open(out) as reloaded:
+ for tag in tags.keys():
+ assert tag not in reloaded.getexif()
+
def test_old_style_jpeg(self):
- infile = "Tests/images/old-style-jpeg-compression.tif"
- with Image.open(infile) as im:
+ with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
+ assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
+
+ def test_open_missing_samplesperpixel(self):
+ with Image.open(
+ "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif"
+ ) as im:
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
def test_no_rows_per_strip(self):
@@ -942,3 +1004,34 @@ class TestFileLibTiff(LibTiffTestCase):
# Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9"
TiffImagePlugin.READ_LIBTIFF = False
+
+ @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
+ def test_save_multistrip(self, compression, tmp_path):
+ im = hopper("RGB").resize((256, 256))
+ out = str(tmp_path / "temp.tif")
+ im.save(out, compression=compression)
+
+ with Image.open(out) as im:
+ # Assert that there are multiple strips
+ assert len(im.tag_v2[STRIPOFFSETS]) > 1
+
+ def test_save_single_strip(self, tmp_path):
+ im = hopper("RGB").resize((256, 256))
+ out = str(tmp_path / "temp.tif")
+
+ TiffImagePlugin.STRIP_SIZE = 2 ** 18
+ try:
+
+ im.save(out, compression="tiff_adobe_deflate")
+
+ with Image.open(out) as im:
+ assert len(im.tag_v2[STRIPOFFSETS]) == 1
+ finally:
+ TiffImagePlugin.STRIP_SIZE = 65536
+
+ @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
+ def test_save_zero(self, compression, tmp_path):
+ im = Image.new("RGB", (0, 0))
+ out = str(tmp_path / "temp.tif")
+ with pytest.raises(SystemError):
+ im.save(out, compression=compression)
diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py
index 25d194b62..e1c1c361b 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -5,9 +5,7 @@ import pytest
from PIL import Image
-from .helper import IMCONVERT, assert_image_equal, hopper, imagemagick_available
-
-_roundtrip = imagemagick_available()
+from .helper import assert_image_equal, hopper, magick_command
def helper_save_as_palm(tmp_path, mode):
@@ -23,13 +21,10 @@ def helper_save_as_palm(tmp_path, mode):
assert os.path.getsize(outfile) > 0
-def open_with_imagemagick(tmp_path, f):
- if not imagemagick_available():
- raise OSError()
-
+def open_with_magick(magick, tmp_path, f):
outfile = str(tmp_path / "temp.png")
rc = subprocess.call(
- [IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
+ magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
)
if rc:
raise OSError
@@ -37,14 +32,15 @@ def open_with_imagemagick(tmp_path, f):
def roundtrip(tmp_path, mode):
- if not _roundtrip:
+ magick = magick_command()
+ if not magick:
return
im = hopper(mode)
outfile = str(tmp_path / "temp.palm")
im.save(outfile)
- converted = open_with_imagemagick(tmp_path, outfile)
+ converted = open_with_magick(magick, tmp_path, outfile)
assert_image_equal(converted, im)
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index e5bba483a..10daa414b 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -8,7 +8,7 @@ import pytest
from PIL import Image, PdfParser
-from .helper import hopper
+from .helper import hopper, mark_if_feature_version
def helper_save_as_pdf(tmp_path, mode, **kwargs):
@@ -30,7 +30,7 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
with open(outfile, "rb") as fp:
contents = fp.read()
size = tuple(
- int(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
+ float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
)
assert im.size == size
@@ -42,7 +42,8 @@ def test_monochrome(tmp_path):
mode = "1"
# Act / Assert
- helper_save_as_pdf(tmp_path, mode)
+ outfile = helper_save_as_pdf(tmp_path, mode)
+ assert os.path.getsize(outfile) < 15000
def test_greyscale(tmp_path):
@@ -85,7 +86,30 @@ def test_unsupported_mode(tmp_path):
im.save(outfile)
-@pytest.mark.valgrind_known_error(reason="Known Failing")
+def test_resolution(tmp_path):
+ im = hopper()
+
+ outfile = str(tmp_path / "temp.pdf")
+ im.save(outfile, resolution=150)
+
+ with open(outfile, "rb") as fp:
+ contents = fp.read()
+
+ size = tuple(
+ float(d)
+ for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ")
+ )
+ assert size == (61.44, 61.44)
+
+ size = tuple(
+ float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split()
+ )
+ assert size == (61.44, 61.44)
+
+
+@mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+)
def test_save_all(tmp_path):
# Single frame image
helper_save_as_pdf(tmp_path, "RGB", save_all=True)
@@ -286,3 +310,14 @@ def test_pdf_append_to_bytesio():
f = io.BytesIO(f.getvalue())
im.save(f, format="PDF", append=True)
assert len(f.getvalue()) > initial_size
+
+
+@pytest.mark.timeout(1)
+@pytest.mark.parametrize("newline", (b"\r", b"\n"))
+def test_redos(newline):
+ malicious = b" trailer<<>>" + newline * 3456
+
+ # This particular exception isn't relevant here.
+ # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292).
+ with pytest.raises(PdfParser.PdfFormatError):
+ PdfParser.PdfParser(buf=malicious)
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index bbf5f5772..0869cc58b 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -1,4 +1,5 @@
import re
+import sys
import zlib
from io import BytesIO
@@ -10,12 +11,18 @@ from .helper import (
PillowLeakTestCase,
assert_image,
assert_image_equal,
+ assert_image_equal_tofile,
hopper,
- is_big_endian,
is_win32,
+ mark_if_feature_version,
skip_unless_feature,
)
+try:
+ import defusedxml.ElementTree as ElementTree
+except ImportError:
+ ElementTree = None
+
# sample png stream
TEST_PNG_FILE = "Tests/images/hopper.png"
@@ -69,7 +76,6 @@ class TestFilePng:
png.crc(cid, s)
return chunks
- @pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
def test_sanity(self, tmp_path):
# internal version number
@@ -383,25 +389,12 @@ class TestFilePng:
# Check dpi roundtripping
with Image.open(TEST_PNG_FILE) as im:
- im = roundtrip(im, dpi=(100, 100))
- assert im.info["dpi"] == (100, 100)
+ im = roundtrip(im, dpi=(100.33, 100.33))
+ assert im.info["dpi"] == (100.33, 100.33)
- def test_load_dpi_rounding(self):
- # Round up
+ def test_load_float_dpi(self):
with Image.open(TEST_PNG_FILE) as im:
- assert im.info["dpi"] == (96, 96)
-
- # Round down
- with Image.open("Tests/images/icc_profile_none.png") as im:
- assert im.info["dpi"] == (72, 72)
-
- def test_save_dpi_rounding(self):
- with Image.open(TEST_PNG_FILE) as im:
- im = roundtrip(im, dpi=(72.2, 72.2))
- assert im.info["dpi"] == (72, 72)
-
- im = roundtrip(im, dpi=(72.8, 72.8))
- assert im.info["dpi"] == (73, 73)
+ assert im.info["dpi"] == (95.9866, 95.9866)
def test_roundtrip_text(self):
# Check text roundtripping
@@ -625,6 +618,23 @@ class TestFilePng:
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
+ def test_padded_idat(self):
+ # This image has been manually hexedited
+ # so that the IDAT chunk has padding at the end
+ # Set MAXBLOCK to the length of the actual data
+ # so that the decoder finishes reading before the chunk ends
+ MAXBLOCK = ImageFile.MAXBLOCK
+ ImageFile.MAXBLOCK = 45
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+
+ with Image.open("Tests/images/padded_idat.png") as im:
+ im.load()
+
+ ImageFile.MAXBLOCK = MAXBLOCK
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
+
+ assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
+
def test_specify_bits(self, tmp_path):
im = hopper("P")
@@ -644,6 +654,18 @@ class TestFilePng:
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 3
+ def test_getxmp(self):
+ with Image.open("Tests/images/color_snakes.png") as im:
+ if ElementTree is None:
+ with pytest.warns(UserWarning):
+ assert im.getxmp() == {}
+ else:
+ xmp = im.getxmp()
+
+ description = xmp["xmpmeta"]["RDF"]["Description"]
+ assert description["PixelXDimension"] == "10"
+ assert description["subject"]["Seq"] is None
+
def test_exif(self):
# With an EXIF chunk
with Image.open("Tests/images/exif.png") as im:
@@ -679,7 +701,9 @@ class TestFilePng:
exif = reloaded._getexif()
assert exif[274] == 1
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_exif_from_jpg(self, tmp_path):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
test_file = str(tmp_path / "temp.png")
@@ -708,6 +732,32 @@ class TestFilePng:
with pytest.raises(EOFError):
im.seek(1)
+ @pytest.mark.parametrize("buffer", (True, False))
+ def test_save_stdout(self, buffer):
+ old_stdout = sys.stdout
+
+ if buffer:
+
+ class MyStdOut:
+ buffer = BytesIO()
+
+ mystdout = MyStdOut()
+ else:
+ mystdout = BytesIO()
+
+ sys.stdout = mystdout
+
+ with Image.open(TEST_PNG_FILE) as im:
+ im.save(sys.stdout, "PNG")
+
+ # Reset stdout
+ sys.stdout = old_stdout
+
+ if buffer:
+ mystdout = mystdout.buffer
+ with Image.open(mystdout) as reloaded:
+ assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
+
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
@skip_unless_feature("zlib")
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index 0ccfb5e88..ad36319db 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -1,3 +1,6 @@
+import sys
+from io import BytesIO
+
import pytest
from PIL import Image
@@ -80,3 +83,30 @@ def test_mimetypes(tmp_path):
f.write("PyCMYK\n128 128\n255")
with Image.open(path) as im:
assert im.get_format_mimetype() == "image/x-portable-anymap"
+
+
+@pytest.mark.parametrize("buffer", (True, False))
+def test_save_stdout(buffer):
+ old_stdout = sys.stdout
+
+ if buffer:
+
+ class MyStdOut:
+ buffer = BytesIO()
+
+ mystdout = MyStdOut()
+ else:
+ mystdout = BytesIO()
+
+ sys.stdout = mystdout
+
+ with Image.open(TEST_FILE) as im:
+ im.save(sys.stdout, "PPM")
+
+ # Reset stdout
+ sys.stdout = old_stdout
+
+ if buffer:
+ mystdout = mystdout.buffer
+ with Image.open(mystdout) as reloaded:
+ assert_image_equal_tofile(reloaded, TEST_FILE)
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
index 87373d2c4..f50fe133f 100644
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -57,9 +57,10 @@ def test_n_frames():
assert im.n_frames == 1
assert not im.is_animated
- with Image.open(test_file) as im:
- assert im.n_frames == 2
- assert im.is_animated
+ for path in [test_file, "Tests/images/negative_layer_count.psd"]:
+ with Image.open(path) as im:
+ assert im.n_frames == 2
+ assert im.is_animated
def test_eoferror():
@@ -130,3 +131,25 @@ def test_combined_larger_than_size():
with pytest.raises(OSError):
with Image.open("Tests/images/combined_larger_than_size.psd"):
pass
+
+
+@pytest.mark.parametrize(
+ "test_file,raises",
+ [
+ (
+ "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd",
+ Image.UnidentifiedImageError,
+ ),
+ (
+ "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd",
+ Image.UnidentifiedImageError,
+ ),
+ ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError),
+ ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
+ ],
+)
+def test_crashes(test_file, raises):
+ with open(test_file, "rb") as f:
+ with pytest.raises(raises):
+ with Image.open(f):
+ pass
diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py
index 0210dd4f1..6a5d8887d 100644
--- a/Tests/test_file_sgi.py
+++ b/Tests/test_file_sgi.py
@@ -73,6 +73,13 @@ def test_write(tmp_path):
img.save(out, format="sgi")
assert_image_equal_tofile(img, out)
+ out = str(tmp_path / "fp.sgi")
+ with open(out, "wb") as fp:
+ img.save(fp)
+ assert_image_equal_tofile(img, out)
+
+ assert not fp.closed
+
for mode in ("L", "RGB", "RGBA"):
roundtrip(hopper(mode))
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index 465e13316..e2351d723 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -6,7 +6,7 @@ import pytest
from PIL import Image
-from .helper import assert_image_equal, hopper
+from .helper import assert_image_equal, assert_image_equal_tofile, hopper
_TGA_DIR = os.path.join("Tests", "images", "tga")
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
@@ -65,6 +65,16 @@ def test_sanity(tmp_path):
roundtrip(original_im)
+def test_palette_depth_16(tmp_path):
+ with Image.open("Tests/images/p_16.tga") as im:
+ assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png")
+
+ out = str(tmp_path / "temp.png")
+ im.save(out)
+ with Image.open(out) as reloaded:
+ assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png")
+
+
def test_id_field():
# tga file with id field
test_file = "Tests/images/tga_id_field.tga"
@@ -112,6 +122,14 @@ def test_save_wrong_mode(tmp_path):
im.save(out)
+def test_save_mapdepth():
+ # This image has been manually hexedited from 200x32_p_bl_raw.tga
+ # to include an origin
+ test_file = "Tests/images/200x32_p_bl_raw_origin.tga"
+ with Image.open(test_file) as im:
+ assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png")
+
+
def test_save_id_section(tmp_path):
test_file = "Tests/images/rgb32rle.tga"
with Image.open(test_file) as im:
@@ -153,6 +171,15 @@ def test_save_orientation(tmp_path):
assert test_im.info["orientation"] == 1
+def test_horizontal_orientations():
+ # These images have been manually hexedited to have the relevant orientations
+ with Image.open("Tests/images/rgb32rle_top_right.tga") as im:
+ assert im.load()[90, 90][:3] == (0, 0, 0)
+
+ with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im:
+ assert im.load()[90, 90][:3] == (0, 255, 0)
+
+
def test_save_rle(tmp_path):
test_file = "Tests/images/rgb32rle.tga"
with Image.open(test_file) as im:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index ba7f9a084..5801e1766 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -3,7 +3,7 @@ from io import BytesIO
import pytest
-from PIL import Image, TiffImagePlugin
+from PIL import Image, ImageFile, TiffImagePlugin
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
from .helper import (
@@ -16,6 +16,11 @@ from .helper import (
is_win32,
)
+try:
+ import defusedxml.ElementTree as ElementTree
+except ImportError:
+ ElementTree = None
+
class TestFileTiff:
def test_sanity(self, tmp_path):
@@ -137,37 +142,33 @@ class TestFileTiff:
im._setup()
assert im.info["dpi"] == (71.0, 71.0)
- def test_load_dpi_rounding(self):
- for resolutionUnit, dpi in ((None, (72, 73)), (2, (72, 73)), (3, (183, 185))):
- with Image.open(
- "Tests/images/hopper_roundDown_" + str(resolutionUnit) + ".tif"
- ) as im:
- assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
- assert im.info["dpi"] == (dpi[0], dpi[0])
+ @pytest.mark.parametrize(
+ "resolutionUnit, dpi",
+ [(None, 72.8), (2, 72.8), (3, 184.912)],
+ )
+ def test_load_float_dpi(self, resolutionUnit, dpi):
+ with Image.open(
+ "Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif"
+ ) as im:
+ assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
+ assert im.info["dpi"] == (dpi, dpi)
- with Image.open(
- "Tests/images/hopper_roundUp_" + str(resolutionUnit) + ".tif"
- ) as im:
- assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
- assert im.info["dpi"] == (dpi[1], dpi[1])
-
- def test_save_dpi_rounding(self, tmp_path):
+ def test_save_float_dpi(self, tmp_path):
outfile = str(tmp_path / "temp.tif")
with Image.open("Tests/images/hopper.tif") as im:
- for dpi in (72.2, 72.8):
- im.save(outfile, dpi=(dpi, dpi))
+ dpi = (72.2, 72.2)
+ im.save(outfile, dpi=dpi)
- with Image.open(outfile) as reloaded:
- reloaded.load()
- assert (round(dpi), round(dpi)) == reloaded.info["dpi"]
+ with Image.open(outfile) as reloaded:
+ assert reloaded.info["dpi"] == dpi
def test_save_setting_missing_resolution(self):
b = BytesIO()
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
im.save(b, format="tiff", resolution=123.45)
with Image.open(b) as im:
- assert float(im.tag_v2[X_RESOLUTION]) == 123.45
- assert float(im.tag_v2[Y_RESOLUTION]) == 123.45
+ assert im.tag_v2[X_RESOLUTION] == 123.45
+ assert im.tag_v2[Y_RESOLUTION] == 123.45
def test_invalid_file(self):
invalid_file = "Tests/images/flower.jpg"
@@ -301,6 +302,19 @@ class TestFileTiff:
assert im.size == (20, 20)
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
+ def test_frame_order(self):
+ # A frame can't progress to itself after reading
+ with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
+ assert im.n_frames == 1
+
+ # A frame can't progress to a frame that has already been read
+ with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
+ assert im.n_frames == 2
+
+ # Frames don't have to be in sequence
+ with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
+ assert im.n_frames == 3
+
def test___str__(self):
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
@@ -389,6 +403,75 @@ class TestFileTiff:
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
assert 0x8825 in im.tag_v2
+ def test_exif(self, tmp_path):
+ def check_exif(exif):
+ assert sorted(exif.keys()) == [
+ 256,
+ 257,
+ 258,
+ 259,
+ 262,
+ 271,
+ 272,
+ 273,
+ 277,
+ 278,
+ 279,
+ 282,
+ 283,
+ 284,
+ 296,
+ 297,
+ 305,
+ 339,
+ 700,
+ 34665,
+ 34853,
+ 50735,
+ ]
+ assert exif[256] == 640
+ assert exif[271] == "FLIR"
+
+ gps = exif.get_ifd(0x8825)
+ assert list(gps.keys()) == [0, 1, 2, 3, 4, 5, 6, 18]
+ assert gps[0] == b"\x03\x02\x00\x00"
+ assert gps[18] == "WGS-84"
+
+ outfile = str(tmp_path / "temp.tif")
+ with Image.open("Tests/images/ifd_tag_type.tiff") as im:
+ exif = im.getexif()
+ check_exif(exif)
+
+ im.save(outfile, exif=exif)
+
+ outfile2 = str(tmp_path / "temp2.tif")
+ with Image.open(outfile) as im:
+ exif = im.getexif()
+ check_exif(exif)
+
+ im.save(outfile2, exif=exif.tobytes())
+
+ with Image.open(outfile2) as im:
+ exif = im.getexif()
+ check_exif(exif)
+
+ def test_exif_frames(self):
+ # Test that EXIF data can change across frames
+ with Image.open("Tests/images/g4-multi.tiff") as im:
+ assert im.getexif()[273] == (328, 815)
+
+ im.seek(1)
+ assert im.getexif()[273] == (1408, 1907)
+
+ @pytest.mark.parametrize("mode", ("1", "L"))
+ def test_photometric(self, mode, tmp_path):
+ filename = str(tmp_path / "temp.tif")
+ im = hopper(mode)
+ im.save(filename, tiffinfo={262: 0})
+ with Image.open(filename) as reloaded:
+ assert reloaded.tag_v2[262] == 0
+ assert_image_equal(im, reloaded)
+
def test_seek(self):
filename = "Tests/images/pil136.tiff"
with Image.open(filename) as im:
@@ -590,6 +673,18 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert "icc_profile" not in reloaded.info
+ def test_getxmp(self):
+ with Image.open("Tests/images/lab.tif") as im:
+ if ElementTree is None:
+ with pytest.warns(UserWarning):
+ assert im.getxmp() == {}
+ else:
+ xmp = im.getxmp()
+
+ description = xmp["xmpmeta"]["RDF"]["Description"]
+ assert description[0]["format"] == "image/tiff"
+ assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
+
def test_close_on_load_exclusive(self, tmp_path):
# similar to test_fd_leak, but runs on unixlike os
tmpfile = str(tmp_path / "temp.tif")
@@ -619,15 +714,25 @@ class TestFileTiff:
# Ignore this UserWarning which triggers for four tags:
# "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..."
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
+ # Ignore this UserWarning:
+ @pytest.mark.filterwarnings("ignore:Truncated File Read")
@pytest.mark.skipif(
not os.path.exists("Tests/images/string_dimension.tiff"),
reason="Extra image files not installed",
)
def test_string_dimension(self):
# Assert that an error is raised if one of the dimensions is a string
- with pytest.raises(ValueError):
- with Image.open("Tests/images/string_dimension.tiff"):
- pass
+ with Image.open("Tests/images/string_dimension.tiff") as im:
+ with pytest.raises(OSError):
+ im.load()
+
+ @pytest.mark.timeout(6)
+ @pytest.mark.filterwarnings("ignore:Truncated File Read")
+ def test_timeout(self):
+ with Image.open("Tests/images/timeout-6646305047838720") as im:
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
+ im.load()
+ ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(not is_win32(), reason="Windows only")
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index 0f7f8adf1..2213af5aa 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -122,7 +122,7 @@ def test_read_metadata():
def test_write_metadata(tmp_path):
- """ Test metadata writing through the python code """
+ """Test metadata writing through the python code"""
with Image.open("Tests/images/hopper.tif") as img:
f = str(tmp_path / "temp.tiff")
img.save(f, tiffinfo=img.tag)
@@ -179,6 +179,27 @@ def test_no_duplicate_50741_tag():
assert TAG_IDS["BestQualityScale"] == 50780
+def test_iptc(tmp_path):
+ out = str(tmp_path / "temp.tiff")
+ with Image.open("Tests/images/hopper.Lab.tif") as im:
+ im.save(out)
+
+
+def test_undefined_zero(tmp_path):
+ # Check that the tag has not been changed since this test was created
+ tag = TiffTags.TAGS_V2[45059]
+ assert tag.type == TiffTags.UNDEFINED
+ assert tag.length == 0
+
+ info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00")
+ info[45059] = b"test"
+
+ # Assert that the tag value does not change by setting it to itself
+ original = info[45059]
+ info[45059] = info[45059]
+ assert info[45059] == original
+
+
def test_empty_metadata():
f = io.BytesIO(b"II*\x00\x08\x00\x00\x00")
head = f.read(8)
@@ -355,3 +376,30 @@ def test_too_many_entries():
# Should not raise ValueError.
pytest.warns(UserWarning, lambda: ifd[277])
+
+
+def test_tag_group_data():
+ base_ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965)
+ for ifd in (base_ifd, interop_ifd):
+ ifd[2] = "test"
+ ifd[256] = 10
+
+ assert base_ifd.tagtype[256] == 4
+ assert interop_ifd.tagtype[256] != base_ifd.tagtype[256]
+
+ assert interop_ifd.tagtype[2] == 7
+ assert base_ifd.tagtype[2] != interop_ifd.tagtype[256]
+
+
+def test_empty_subifd(tmp_path):
+ out = str(tmp_path / "temp.jpg")
+
+ im = hopper()
+ exif = im.getexif()
+ exif[TiffImagePlugin.EXIFIFD] = {}
+ im.save(out, exif=exif)
+
+ with Image.open(out) as reloaded:
+ exif = reloaded.getexif()
+ assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {}
diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py
index 60be1d5bc..f25b42fe0 100644
--- a/Tests/test_file_wal.py
+++ b/Tests/test_file_wal.py
@@ -1,15 +1,21 @@
from PIL import WalImageFile
+from .helper import assert_image_equal_tofile
+
def test_open():
# Arrange
TEST_FILE = "Tests/images/hopper.wal"
# Act
- im = WalImageFile.open(TEST_FILE)
+ with WalImageFile.open(TEST_FILE) as im:
- # Assert
- assert im.format == "WAL"
- assert im.format_description == "Quake2 Texture"
- assert im.mode == "P"
- assert im.size == (128, 128)
+ # Assert
+ assert im.format == "WAL"
+ assert im.format_description == "Quake2 Texture"
+ assert im.mode == "P"
+ assert im.size == (128, 128)
+
+ assert isinstance(im, WalImageFile.WalImageFile)
+
+ assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index cde7020ed..e72b4993c 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -1,5 +1,6 @@
import io
import re
+import sys
import pytest
@@ -103,6 +104,13 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
+ def test_icc_profile(self, tmp_path):
+ self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
+ if _webp.HAVE_WEBPANIM:
+ self._roundtrip(
+ tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
+ )
+
def test_write_unsupported_mode_L(self, tmp_path):
"""
Saving a black-and-white file to WebP format should work, and be
@@ -119,6 +127,14 @@ class TestFileWebp:
self._roundtrip(tmp_path, "P", 50.0)
+ @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
+ def test_write_encoding_error_message(self, tmp_path):
+ temp_file = str(tmp_path / "temp.webp")
+ im = Image.new("RGB", (15000, 15000))
+ with pytest.raises(ValueError) as e:
+ im.save(temp_file, method=0)
+ assert str(e.value) == "encoding error 6"
+
def test_WebPEncode_with_invalid_args(self):
"""
Calling encoder functions with no arguments should result in an error.
@@ -172,9 +188,7 @@ class TestFileWebp:
with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1))
- difference = sum(
- [abs(original_value[i] - reread_value[i]) for i in range(0, 3)]
- )
+ difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
assert difference < 5
@skip_unless_feature("webp")
diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py
index 26e903488..25ebffe02 100644
--- a/Tests/test_file_webp_animated.py
+++ b/Tests/test_file_webp_animated.py
@@ -45,12 +45,12 @@ def test_write_animation_L(tmp_path):
# Compare first and last frames to the original animated GIF
orig.load()
im.load()
- assert_image_similar(im, orig.convert("RGBA"), 25.0)
+ assert_image_similar(im, orig.convert("RGBA"), 32.9)
orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1)
orig.load()
im.load()
- assert_image_similar(im, orig.convert("RGBA"), 25.0)
+ assert_image_similar(im, orig.convert("RGBA"), 32.9)
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index cb133e2c5..e6d6fc63f 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -4,7 +4,7 @@ import pytest
from PIL import Image
-from .helper import skip_unless_feature
+from .helper import mark_if_feature_version, skip_unless_feature
pytestmark = [
skip_unless_feature("webp"),
@@ -41,7 +41,9 @@ def test_read_exif_metadata_without_prefix():
assert exif[305] == "Adobe Photoshop CS6 (Macintosh)"
-@pytest.mark.valgrind_known_error(reason="Known Failing")
+@mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+)
def test_write_exif_metadata():
file_path = "Tests/images/flower.jpg"
test_buffer = BytesIO()
@@ -74,7 +76,9 @@ def test_read_icc_profile():
assert icc == expected_icc
-@pytest.mark.valgrind_known_error(reason="Known Failing")
+@mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+)
def test_write_icc_metadata():
file_path = "Tests/images/flower2.jpg"
test_buffer = BytesIO()
@@ -92,7 +96,9 @@ def test_write_icc_metadata():
assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match"
-@pytest.mark.valgrind_known_error(reason="Known Failing")
+@mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+)
def test_read_no_exif():
file_path = "Tests/images/flower.jpg"
test_buffer = BytesIO()
diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py
index bf9d105e5..3f8bc96cc 100644
--- a/Tests/test_file_wmf.py
+++ b/Tests/test_file_wmf.py
@@ -44,14 +44,9 @@ def test_register_handler(tmp_path):
WmfImagePlugin.register_handler(original_handler)
-def test_load_dpi_rounding():
- # Round up
+def test_load_float_dpi():
with Image.open("Tests/images/drawing.emf") as im:
- assert im.info["dpi"] == 1424
-
- # Round down
- with Image.open("Tests/images/drawing_roundDown.emf") as im:
- assert im.info["dpi"] == 1426
+ assert im.info["dpi"] == 1423.7668161434979
def test_load_set_dpi():
diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py
index 015210b4d..38f7ddac5 100644
--- a/Tests/test_font_leaks.py
+++ b/Tests/test_font_leaks.py
@@ -4,7 +4,7 @@ from .helper import PillowLeakTestCase, skip_unless_feature
class TestTTypeFontLeak(PillowLeakTestCase):
- # fails at iteration 3 in master
+ # fails at iteration 3 in main
iterations = 10
mem_limit = 4096 # k
@@ -24,7 +24,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
class TestDefaultFontLeak(TestTTypeFontLeak):
- # fails at iteration 37 in master
+ # fails at iteration 37 in main
iterations = 100
mem_limit = 1024 # k
diff --git a/Tests/test_image.py b/Tests/test_image.py
index 30d093e15..4dde66f11 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -6,8 +6,7 @@ import tempfile
import pytest
-import PIL
-from PIL import Image, ImageDraw, ImagePalette, ImageShow, UnidentifiedImageError
+from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError
from .helper import (
assert_image_equal,
@@ -16,6 +15,7 @@ from .helper import (
assert_not_all_same,
hopper,
is_win32,
+ mark_if_feature_version,
skip_unless_feature,
)
@@ -148,10 +148,11 @@ class TestImage:
assert im.mode == "RGB"
assert im.size == (128, 128)
- temp_file = str(tmp_path / "temp.jpg")
- if os.path.exists(temp_file):
- os.remove(temp_file)
- im.save(Path(temp_file))
+ for ext in (".jpg", ".jp2"):
+ temp_file = str(tmp_path / ("temp." + ext))
+ if os.path.exists(temp_file):
+ os.remove(temp_file)
+ im.save(Path(temp_file))
def test_fp_name(self, tmp_path):
temp_file = str(tmp_path / "temp.jpg")
@@ -192,6 +193,10 @@ class TestImage:
assert not im.readonly
@pytest.mark.skipif(is_win32(), reason="Test requires opening tempfile twice")
+ @pytest.mark.skipif(
+ sys.platform == "cygwin",
+ reason="Test requires opening an mmaped file for writing",
+ )
def test_readonly_save(self, tmp_path):
temp_file = str(tmp_path / "temp.bmp")
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
@@ -581,6 +586,10 @@ class TestImage:
assert ext_individual == ext_multiple
def test_remap_palette(self):
+ # Test identity transform
+ with Image.open("Tests/images/hopper.gif") as im:
+ assert_image_equal(im, im.remap_palette(list(range(256))))
+
# Test illegal image mode
with hopper() as im:
with pytest.raises(ValueError):
@@ -605,7 +614,7 @@ class TestImage:
else:
assert new_im.palette is None
- _make_new(im, im_p, im_p.palette)
+ _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
@@ -620,22 +629,6 @@ class TestImage:
expected = Image.new(mode, (100, 100), color)
assert_image_equal(im.convert(mode), expected)
- def test_showxv_deprecation(self):
- class TestViewer(ImageShow.Viewer):
- def show_image(self, image, **options):
- return True
-
- viewer = TestViewer()
- ImageShow.register(viewer, -1)
-
- im = Image.new("RGB", (50, 50), "white")
-
- with pytest.warns(DeprecationWarning):
- Image._showxv(im)
-
- # Restore original state
- ImageShow._viewers.pop(0)
-
def test_no_resource_warning_on_save(self, tmp_path):
# https://github.com/python-pillow/Pillow/issues/835
# Arrange
@@ -662,7 +655,9 @@ class TestImage:
assert not fp.closed
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_exif_jpeg(self, tmp_path):
with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian
exif = im.getexif()
@@ -770,9 +765,27 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769)
- @pytest.mark.skipif(
- sys.version_info < (3, 7), reason="Python 3.7 or greater required"
- )
+ def test_exif_load_from_fp(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ data = im.info["exif"]
+ if data.startswith(b"Exif\x00\x00"):
+ data = data[6:]
+ fp = io.BytesIO(data)
+
+ exif = Image.Exif()
+ exif.load_from_fp(fp)
+ assert exif == {
+ 271: "Canon",
+ 272: "Canon PowerShot S40",
+ 274: 1,
+ 282: 180.0,
+ 283: 180.0,
+ 296: 2,
+ 306: "2003:12:14 12:01:44",
+ 531: 1,
+ 34665: 196,
+ }
+
def test_categories_deprecation(self):
with pytest.warns(DeprecationWarning):
assert hopper().category == 0
@@ -784,35 +797,6 @@ class TestImage:
with pytest.warns(DeprecationWarning):
assert Image.CONTAINER == 2
- @pytest.mark.parametrize(
- "test_module",
- [PIL, Image],
- )
- def test_pillow_version(self, test_module):
- with pytest.warns(DeprecationWarning):
- assert test_module.PILLOW_VERSION == PIL.__version__
-
- with pytest.warns(DeprecationWarning):
- str(test_module.PILLOW_VERSION)
-
- with pytest.warns(DeprecationWarning):
- assert int(test_module.PILLOW_VERSION[0]) >= 7
-
- with pytest.warns(DeprecationWarning):
- assert test_module.PILLOW_VERSION < "9.9.0"
-
- with pytest.warns(DeprecationWarning):
- assert test_module.PILLOW_VERSION <= "9.9.0"
-
- with pytest.warns(DeprecationWarning):
- assert test_module.PILLOW_VERSION != "7.0.0"
-
- with pytest.warns(DeprecationWarning):
- assert test_module.PILLOW_VERSION >= "7.0.0"
-
- with pytest.warns(DeprecationWarning):
- assert test_module.PILLOW_VERSION > "7.0.0"
-
@pytest.mark.parametrize(
"path",
[
@@ -848,18 +832,6 @@ class TestImage:
except OSError as e:
assert str(e) == "buffer overrun when reading image file"
- def test_show_deprecation(self, monkeypatch):
- monkeypatch.setattr(Image, "_show", lambda *args, **kwargs: None)
-
- im = Image.new("RGB", (50, 50), "white")
-
- with pytest.warns(None) as raised:
- im.show()
- assert not raised
-
- with pytest.warns(DeprecationWarning):
- im.show(command="mock")
-
class MockEncoder:
pass
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index e86dc8530..7b3036979 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -23,6 +23,11 @@ else:
except ImportError:
cffi = None
+try:
+ import numpy
+except ImportError:
+ numpy = None
+
class AccessTest:
# initial value
@@ -66,6 +71,10 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
+ for x, y in ((0, "0"), ("0", 0)):
+ with pytest.raises(TypeError):
+ pix1[x, y]
+
for y in range(im1.size[1]):
for x in range(im1.size[0]):
pix2[x, y] = pix1[x, y]
@@ -109,6 +118,13 @@ class TestImagePutPixel(AccessTest):
assert_image_equal(im1, im2)
+ @pytest.mark.skipif(numpy is None, reason="NumPy not installed")
+ def test_numpy(self):
+ im = hopper()
+ pix = im.load()
+
+ assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
+
class TestImageGetPixel(AccessTest):
@staticmethod
@@ -339,6 +355,24 @@ class TestImagePutPixelError(AccessTest):
with pytest.raises(TypeError, match="color must be int or tuple"):
im.putpixel((0, 0), v)
+ @pytest.mark.parametrize(
+ ("mode", "band_numbers", "match"),
+ (
+ ("L", (0, 2), "color must be int or single-element tuple"),
+ ("LA", (0, 3), "color must be int, or tuple of one or two elements"),
+ (
+ "RGB",
+ (0, 2, 5),
+ "color must be int, or tuple of one, three or four elements",
+ ),
+ ),
+ )
+ def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match):
+ im = hopper(mode)
+ for band_number in band_numbers:
+ with pytest.raises(TypeError, match=match):
+ im.putpixel((0, 0), (0,) * band_number)
+
@pytest.mark.parametrize("mode", IMAGE_MODES2)
def test_putpixel_type_error2(self, mode):
im = hopper(mode)
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index 980458407..5c9cdd7e0 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -4,31 +4,44 @@ from PIL import Image
from .helper import hopper
+numpy = pytest.importorskip("numpy", reason="NumPy not installed")
+
im = hopper().resize((128, 100))
def test_toarray():
def test(mode):
- ai = im.convert(mode).__array_interface__
- return ai["version"], ai["shape"], ai["typestr"], len(ai["data"])
+ ai = numpy.array(im.convert(mode))
+ return ai.shape, ai.dtype.str, ai.nbytes
- # assert test("1") == (3, (100, 128), '|b1', 1600))
- assert test("L") == (3, (100, 128), "|u1", 12800)
+ def test_with_dtype(dtype):
+ ai = numpy.array(im, dtype=dtype)
+ assert ai.dtype == dtype
+
+ # assert test("1") == ((100, 128), '|b1', 1600))
+ assert test("L") == ((100, 128), "|u1", 12800)
# FIXME: wrong?
- assert test("I") == (3, (100, 128), Image._ENDIAN + "i4", 51200)
+ assert test("I") == ((100, 128), Image._ENDIAN + "i4", 51200)
# FIXME: wrong?
- assert test("F") == (3, (100, 128), Image._ENDIAN + "f4", 51200)
+ assert test("F") == ((100, 128), Image._ENDIAN + "f4", 51200)
- assert test("LA") == (3, (100, 128, 2), "|u1", 25600)
- assert test("RGB") == (3, (100, 128, 3), "|u1", 38400)
- assert test("RGBA") == (3, (100, 128, 4), "|u1", 51200)
- assert test("RGBX") == (3, (100, 128, 4), "|u1", 51200)
+ assert test("LA") == ((100, 128, 2), "|u1", 25600)
+ assert test("RGB") == ((100, 128, 3), "|u1", 38400)
+ assert test("RGBA") == ((100, 128, 4), "|u1", 51200)
+ assert test("RGBX") == ((100, 128, 4), "|u1", 51200)
+
+ test_with_dtype(numpy.float64)
+ test_with_dtype(numpy.uint8)
+
+ with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
+ with pytest.raises(OSError):
+ numpy.array(im_truncated)
def test_fromarray():
class Wrapper:
- """ Class with API matching Image.fromarray """
+ """Class with API matching Image.fromarray"""
def __init__(self, img, arr_params):
self.img = img
@@ -39,10 +52,18 @@ def test_fromarray():
def test(mode):
i = im.convert(mode)
- a = i.__array_interface__
- a["strides"] = 1 # pretend it's non-contiguous
+ a = numpy.array(i)
# Make wrapper instance for image, new array interface
- wrapped = Wrapper(i, a)
+ wrapped = Wrapper(
+ i,
+ {
+ "shape": a.shape,
+ "typestr": a.dtype.str,
+ "version": 3,
+ "data": a.data,
+ "strides": 1, # pretend it's non-contiguous
+ },
+ )
out = Image.fromarray(wrapped)
return out.mode, out.size, list(i.getdata()) == list(out.getdata())
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 6fe1bd962..a5a95e962 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -41,11 +41,15 @@ def test_sanity():
def test_default():
im = hopper("P")
- assert_image(im, "P", im.size)
- im = im.convert()
- assert_image(im, "RGB", im.size)
- im = im.convert()
- assert_image(im, "RGB", im.size)
+ assert im.mode == "P"
+ converted_im = im.convert()
+ assert_image(converted_im, "RGB", im.size)
+ converted_im = im.convert()
+ assert_image(converted_im, "RGB", im.size)
+
+ im.info["transparency"] = 0
+ converted_im = im.convert()
+ assert_image(converted_im, "RGBA", im.size)
# ref https://github.com/python-pillow/Pillow/issues/274
@@ -89,29 +93,33 @@ def test_trns_p(tmp_path):
f = str(tmp_path / "temp.png")
im_l = im.convert("L")
- assert im_l.info["transparency"] == 0 # undone
+ assert im_l.info["transparency"] == 1 # undone
im_l.save(f)
im_rgb = im.convert("RGB")
- assert im_rgb.info["transparency"] == (0, 0, 0) # undone
+ assert im_rgb.info["transparency"] == (0, 1, 2) # undone
im_rgb.save(f)
# ref https://github.com/python-pillow/Pillow/issues/664
-def test_trns_p_rgba():
+@pytest.mark.parametrize("mode", ("LA", "PA", "RGBA"))
+def test_trns_p_transparency(mode):
# Arrange
im = hopper("P")
im.info["transparency"] = 128
# Act
- im_rgba = im.convert("RGBA")
+ converted_im = im.convert(mode)
# Assert
- assert "transparency" not in im_rgba.info
- # https://github.com/python-pillow/Pillow/issues/2702
- assert im_rgba.palette is None
+ assert "transparency" not in converted_im.info
+ if mode == "PA":
+ assert converted_im.palette is not None
+ else:
+ # https://github.com/python-pillow/Pillow/issues/2702
+ assert converted_im.palette is None
def test_trns_l(tmp_path):
@@ -128,8 +136,8 @@ def test_trns_l(tmp_path):
assert "transparency" in im_p.info
im_p.save(f)
- im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE)
- assert "transparency" not in im_p.info
+ im_p = im.convert("P", palette=Image.ADAPTIVE)
+ assert "transparency" in im_p.info
im_p.save(f)
@@ -155,13 +163,33 @@ def test_trns_RGB(tmp_path):
assert "transparency" not in im_p.info
im_p.save(f)
+ im = Image.new("RGB", (1, 1))
+ im.info["transparency"] = im.getpixel((0, 0))
+ im_p = im.convert("P", palette=Image.ADAPTIVE)
+ assert im_p.info["transparency"] == im_p.getpixel((0, 0))
+ im_p.save(f)
+
+
+@pytest.mark.parametrize("convert_mode", ("L", "LA", "I"))
+def test_l_macro_rounding(convert_mode):
+ for mode in ("P", "PA"):
+ im = Image.new(mode, (1, 1))
+ im.palette.getcolor((0, 1, 2))
+
+ converted_im = im.convert(convert_mode)
+ px = converted_im.load()
+ converted_color = px[0, 0]
+ if convert_mode == "LA":
+ converted_color = converted_color[0]
+ assert converted_color == 1
+
def test_gif_with_rgba_palette_to_p():
# See https://github.com/python-pillow/Pillow/issues/2433
with Image.open("Tests/images/hopper.gif") as im:
im.info["transparency"] = 255
im.load()
- assert im.palette.mode == "RGBA"
+ assert im.palette.mode == "RGB"
im_p = im.convert("P")
# Should not raise ValueError: unrecognized raw mode
diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py
index 3740fbcdc..1d3ca8135 100644
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -236,7 +236,7 @@ class TestImagingPaste:
[
(127, 191, 254, 191),
(111, 207, 206, 110),
- (255, 255, 255, 0) if mode == "RGBA" else (127, 254, 127, 0),
+ (127, 254, 127, 0),
(207, 207, 239, 239),
(191, 191, 190, 191),
(207, 206, 111, 112),
diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py
index 51108ead2..366f45854 100644
--- a/Tests/test_image_point.py
+++ b/Tests/test_image_point.py
@@ -32,7 +32,7 @@ def test_16bit_lut():
def test_f_lut():
- """ Tests for floating point lut of 8bit gray image """
+ """Tests for floating point lut of 8bit gray image"""
im = hopper("L")
lut = [0.5 * float(x) for x in range(256)]
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index 54712fd6c..7e4bbaaec 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -1,6 +1,8 @@
import sys
from array import array
+import pytest
+
from PIL import Image
from .helper import assert_image_equal, hopper
@@ -47,6 +49,12 @@ def test_pypy_performance():
im.putdata(list(range(256)) * 256)
+def test_mode_with_L_with_float():
+ im = Image.new("L", (1, 1), 0)
+ im.putdata([2.0])
+ assert im.getpixel((0, 0)) == 2
+
+
def test_mode_i():
src = hopper("L")
data = list(src.getdata())
@@ -87,3 +95,18 @@ def test_array_F():
im.putdata(arr)
assert len(im.getdata()) == len(arr)
+
+
+def test_not_flattened():
+ im = Image.new("L", (1, 1))
+ with pytest.raises(TypeError):
+ im.putdata([[0]])
+ with pytest.raises(TypeError):
+ im.putdata([[0]], 2)
+
+ with pytest.raises(TypeError):
+ im = Image.new("I", (1, 1))
+ im.putdata([[0]])
+ with pytest.raises(TypeError):
+ im = Image.new("F", (1, 1))
+ im.putdata([[0]])
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
index 5a9df11b1..012a57a09 100644
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -2,7 +2,7 @@ import pytest
from PIL import Image, ImagePalette
-from .helper import assert_image_equal, hopper
+from .helper import assert_image_equal, assert_image_equal_tofile, hopper
def test_putpalette():
@@ -36,9 +36,15 @@ def test_putpalette():
def test_imagepalette():
im = hopper("P")
im.putpalette(ImagePalette.negative())
+ assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png")
+
im.putpalette(ImagePalette.random())
+
im.putpalette(ImagePalette.sepia())
+ assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png")
+
im.putpalette(ImagePalette.wedge())
+ assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png")
def test_putpalette_with_alpha_values():
diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py
index af4172c88..53b6c9007 100644
--- a/Tests/test_image_quantize.py
+++ b/Tests/test_image_quantize.py
@@ -2,18 +2,18 @@ import pytest
from PIL import Image
-from .helper import assert_image, assert_image_similar, hopper, is_ppc64le
+from .helper import assert_image_similar, hopper, is_ppc64le
def test_sanity():
image = hopper()
converted = image.quantize()
- assert_image(converted, "P", converted.size)
+ assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 10)
image = hopper()
converted = image.quantize(palette=hopper("P"))
- assert_image(converted, "P", converted.size)
+ assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 60)
@@ -27,7 +27,7 @@ def test_libimagequant_quantize():
pytest.skip("libimagequant support not available")
else:
raise
- assert_image(converted, "P", converted.size)
+ assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 15)
assert len(converted.getcolors()) == 100
@@ -35,7 +35,7 @@ def test_libimagequant_quantize():
def test_octree_quantize():
image = hopper()
converted = image.quantize(100, Image.FASTOCTREE)
- assert_image(converted, "P", converted.size)
+ assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 20)
assert len(converted.getcolors()) == 100
@@ -52,7 +52,7 @@ def test_quantize():
with Image.open("Tests/images/caption_6_33_22.png") as image:
image = image.convert("RGB")
converted = image.quantize()
- assert_image(converted, "P", converted.size)
+ assert converted.mode == "P"
assert_image_similar(converted.convert("RGB"), image, 1)
@@ -62,7 +62,8 @@ def test_quantize_no_dither():
palette = palette.convert("P")
converted = image.quantize(dither=0, palette=palette)
- assert_image(converted, "P", converted.size)
+ assert converted.mode == "P"
+ assert converted.palette.palette == palette.palette.palette
def test_quantize_dither_diff():
@@ -74,3 +75,37 @@ def test_quantize_dither_diff():
nodither = image.quantize(dither=0, palette=palette)
assert dither.tobytes() != nodither.tobytes()
+
+
+def test_colors():
+ im = hopper()
+ colors = 2
+ converted = im.quantize(colors)
+ assert len(converted.palette.palette) == colors * len("RGB")
+
+
+def test_transparent_colors_equal():
+ im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
+ px = im.load()
+ px[0, 1] = (255, 255, 255, 0)
+
+ converted = im.quantize()
+ converted_px = converted.load()
+ assert converted_px[0, 0] == converted_px[0, 1]
+
+
+@pytest.mark.parametrize(
+ "method, color",
+ (
+ (Image.MEDIANCUT, (0, 0, 0)),
+ (Image.MAXCOVERAGE, (0, 0, 0)),
+ (Image.FASTOCTREE, (0, 0, 0)),
+ (Image.FASTOCTREE, (0, 0, 0, 0)),
+ ),
+)
+def test_palette(method, color):
+ im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color)
+
+ converted = im.quantize(method=method)
+ converted_px = converted.load()
+ assert converted_px[0, 0] == converted.palette.colors[color]
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index 69449198e..8bf2ce916 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -4,7 +4,12 @@ import pytest
from PIL import Image, ImageDraw
-from .helper import assert_image_equal, assert_image_similar, hopper
+from .helper import (
+ assert_image_equal,
+ assert_image_similar,
+ hopper,
+ mark_if_feature_version,
+)
class TestImagingResampleVulnerability:
@@ -455,7 +460,9 @@ class TestCoreResampleBox:
tiled.paste(tile, (x0, y0))
return tiled
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_tiles(self):
with Image.open("Tests/images/flower.jpg") as im:
assert im.size == (480, 360)
@@ -466,7 +473,9 @@ class TestCoreResampleBox:
tiled = self.resize_tiled(im, dst_size, *tiles)
assert_image_similar(reference, tiled, 0.01)
- @pytest.mark.valgrind_known_error(reason="Known Failing")
+ @mark_if_feature_version(
+ pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
+ )
def test_subsample(self):
# This test shows advantages of the subpixel resizing
# after supersampling (e.g. during JPEG decoding).
diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py
index a49abe1b9..1fe278052 100644
--- a/Tests/test_image_resize.py
+++ b/Tests/test_image_resize.py
@@ -7,7 +7,12 @@ import pytest
from PIL import Image
-from .helper import assert_image_equal, assert_image_similar, hopper
+from .helper import (
+ assert_image_equal,
+ assert_image_equal_tofile,
+ assert_image_similar,
+ hopper,
+)
class TestImagingCoreResize:
@@ -135,11 +140,22 @@ class TestImagingCoreResize:
with pytest.raises(ValueError):
self.resize(hopper(), (10, 10), 9)
+ def test_cross_platform(self, tmp_path):
+ # This test is intended for only check for consistent behaviour across
+ # platforms. So if a future Pillow change requires that the test file
+ # be updated, that is okay.
+ im = hopper().resize((64, 64))
+ temp_file = str(tmp_path / "temp.gif")
+ im.save(temp_file)
+
+ with Image.open(temp_file) as reloaded:
+ assert_image_equal_tofile(reloaded, "Tests/images/hopper_resized.gif")
+
@pytest.fixture
def gradients_image():
- im = Image.open("Tests/images/radial_gradients.png")
- im.load()
+ with Image.open("Tests/images/radial_gradients.png") as im:
+ im.load()
try:
yield im
finally:
@@ -250,3 +266,7 @@ class TestImageResize:
for mode in "1", "P":
im = hopper(mode)
assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20))
+
+ for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16":
+ im = hopper(mode)
+ assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20))
diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py
index 79ed79042..2d72ffa68 100644
--- a/Tests/test_image_rotate.py
+++ b/Tests/test_image_rotate.py
@@ -33,6 +33,9 @@ def test_angle():
with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
+ im = hopper()
+ assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
+
def test_zero():
for angle in (0, 45, 90, 180, 270):
diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py
index 6911ce460..dd140955d 100644
--- a/Tests/test_image_thumbnail.py
+++ b/Tests/test_image_thumbnail.py
@@ -88,6 +88,7 @@ def test_no_resize():
assert im.size == (64, 64)
+# valgrind test is failing with memory allocated in libjpeg
@pytest.mark.valgrind_known_error(reason="Known Failing")
def test_DCT_scaling_edges():
# Make an image with red borders and size (N * 8) + 1 to cross DCT grid
diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py
index 845900267..ea208362b 100644
--- a/Tests/test_image_transform.py
+++ b/Tests/test_image_transform.py
@@ -32,6 +32,11 @@ class TestImageTransform:
new_im = im.transform((100, 100), transform)
assert new_im.info["comment"] == comment
+ def test_palette(self):
+ with Image.open("Tests/images/hopper.gif") as im:
+ transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0])
+ assert im.palette.palette == transformed.palette.palette
+
def test_extent(self):
im = hopper("RGB")
(w, h) = im.size
diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py
index a19fbf239..b839a7b14 100644
--- a/Tests/test_imagechops.py
+++ b/Tests/test_imagechops.py
@@ -368,11 +368,11 @@ def test_subtract_modulo_no_clip():
def test_soft_light():
# Arrange
- im1 = Image.open("Tests/images/hopper.png")
- im2 = Image.open("Tests/images/hopper-XYZ.png")
+ with Image.open("Tests/images/hopper.png") as im1:
+ with Image.open("Tests/images/hopper-XYZ.png") as im2:
- # Act
- new = ImageChops.soft_light(im1, im2)
+ # Act
+ new = ImageChops.soft_light(im1, im2)
# Assert
assert new.getpixel((64, 64)) == (163, 54, 32)
@@ -381,11 +381,11 @@ def test_soft_light():
def test_hard_light():
# Arrange
- im1 = Image.open("Tests/images/hopper.png")
- im2 = Image.open("Tests/images/hopper-XYZ.png")
+ with Image.open("Tests/images/hopper.png") as im1:
+ with Image.open("Tests/images/hopper-XYZ.png") as im2:
- # Act
- new = ImageChops.hard_light(im1, im2)
+ # Act
+ new = ImageChops.hard_light(im1, im2)
# Assert
assert new.getpixel((64, 64)) == (144, 50, 27)
@@ -394,11 +394,11 @@ def test_hard_light():
def test_overlay():
# Arrange
- im1 = Image.open("Tests/images/hopper.png")
- im2 = Image.open("Tests/images/hopper-XYZ.png")
+ with Image.open("Tests/images/hopper.png") as im1:
+ with Image.open("Tests/images/hopper-XYZ.png") as im2:
- # Act
- new = ImageChops.overlay(im1, im2)
+ # Act
+ new = ImageChops.overlay(im1, im2)
# Assert
assert new.getpixel((64, 64)) == (159, 50, 27)
diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py
index b5d693796..dcc44e6e3 100644
--- a/Tests/test_imagecolor.py
+++ b/Tests/test_imagecolor.py
@@ -191,3 +191,12 @@ def test_rounding_errors():
assert (255, 255) == ImageColor.getcolor("white", "LA")
assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")
Image.new("LA", (1, 1), "white")
+
+
+def test_color_too_long():
+ # Arrange
+ color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)"
+
+ # Act / Assert
+ with pytest.raises(ValueError):
+ ImageColor.getrgb(color_too_long)
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 06c5b2503..b661494c7 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -467,6 +467,23 @@ def test_shape2():
assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png")
+def test_transform():
+ # Arrange
+ im = Image.new("RGB", (100, 100), "white")
+ expected = im.copy()
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ s = ImageDraw.Outline()
+ s.line(0, 0)
+ s.transform((0, 0, 0, 0, 0, 0))
+
+ draw.shape(s, fill=1)
+
+ # Assert
+ assert_image_equal(im, expected)
+
+
def helper_pieslice(bbox, start, end):
# Arrange
im = Image.new("RGB", (W, H))
@@ -538,6 +555,36 @@ def test_pieslice_wide():
assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png")
+def test_pieslice_no_spikes():
+ im = Image.new("RGB", (161, 161), "white")
+ draw = ImageDraw.Draw(im)
+ cxs = (
+ [140] * 3
+ + list(range(140, 19, -20))
+ + [20] * 5
+ + list(range(20, 141, 20))
+ + [140] * 2
+ )
+ cys = (
+ list(range(80, 141, 20))
+ + [140] * 5
+ + list(range(140, 19, -20))
+ + [20] * 5
+ + list(range(20, 80, 20))
+ )
+
+ for cx, cy, angle in zip(cxs, cys, range(0, 360, 15)):
+ draw.pieslice(
+ [cx - 100, cy - 100, cx + 100, cy + 100], angle, angle + 1, fill="black"
+ )
+ draw.point([cx, cy], fill="red")
+
+ im_pre_erase = im.copy()
+ draw.rectangle([21, 21, 139, 139], fill="white")
+
+ assert_image_equal(im, im_pre_erase)
+
+
def helper_point(points):
# Arrange
im = Image.new("RGB", (W, H))
@@ -608,6 +655,19 @@ def test_polygon_1px_high():
assert_image_equal_tofile(im, expected)
+def test_polygon_translucent():
+ # Arrange
+ im = Image.new("RGB", (W, H))
+ draw = ImageDraw.Draw(im, "RGBA")
+
+ # Act
+ draw.polygon([(20, 80), (80, 80), (80, 20)], fill=(0, 255, 0, 127))
+
+ # Assert
+ expected = "Tests/images/imagedraw_polygon_translucent.png"
+ assert_image_equal_tofile(im, expected)
+
+
def helper_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
@@ -722,6 +782,29 @@ def test_rounded_rectangle(xy):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png")
+@pytest.mark.parametrize(
+ "xy, radius, type",
+ [
+ ((10, 20, 190, 180), 30.5, "given"),
+ ((10, 10, 181, 190), 90, "width"),
+ ((10, 20, 190, 181), 85, "height"),
+ ],
+)
+def test_rounded_rectangle_non_integer_radius(xy, radius, type):
+ # Arrange
+ im = Image.new("RGB", (200, 200))
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.rounded_rectangle(xy, radius, fill="red", outline="green", width=5)
+
+ # Assert
+ assert_image_equal_tofile(
+ im,
+ "Tests/images/imagedraw_rounded_rectangle_non_integer_radius_" + type + ".png",
+ )
+
+
def test_rounded_rectangle_zero_radius():
# Arrange
im = Image.new("RGB", (W, H))
@@ -879,6 +962,18 @@ def test_triangle_right():
)
+@pytest.mark.parametrize(
+ "fill, suffix",
+ ((BLACK, "width"), (None, "width_no_fill")),
+)
+def test_triangle_right_width(fill, suffix):
+ img, draw = create_base_image_draw((100, 100))
+ draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5)
+ assert_image_equal_tofile(
+ img, os.path.join(IMAGES_PATH, "triangle_right_" + suffix + ".png")
+ )
+
+
def test_line_horizontal():
img, draw = create_base_image_draw((20, 20))
draw.line((5, 5, 14, 5), BLACK, 2)
@@ -1326,3 +1421,22 @@ def test_compute_regular_polygon_vertices_input_error_handling(
with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
assert str(e.value) == error_message
+
+
+def test_continuous_horizontal_edges_polygon():
+ xy = [
+ (2, 6),
+ (6, 6),
+ (12, 6),
+ (12, 12),
+ (8, 12),
+ (8, 8),
+ (4, 8),
+ (2, 8),
+ ]
+ img, draw = create_base_image_draw((16, 16))
+ draw.polygon(xy, BLACK)
+ expected = os.path.join(IMAGES_PATH, "continuous_horizontal_edges_polygon.png")
+ assert_image_equal_tofile(
+ img, expected, "continuous horizontal edges polygon failed"
+ )
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index b4107e8e3..a5c76700d 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -2,7 +2,7 @@ from io import BytesIO
import pytest
-from PIL import EpsImagePlugin, Image, ImageFile, features
+from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features
from .helper import (
assert_image,
@@ -82,6 +82,19 @@ class TestImageFile:
p.feed(data)
assert (48, 48) == p.image.size
+ @skip_unless_feature("webp")
+ @skip_unless_feature("webp_anim")
+ def test_incremental_webp(self):
+ with ImageFile.Parser() as p:
+ with open("Tests/images/hopper.webp", "rb") as f:
+ p.feed(f.read(1024))
+
+ # Check that insufficient data was given in the first feed
+ assert not p.image
+
+ p.feed(f.read())
+ assert (128, 128) == p.image.size
+
@skip_unless_feature("zlib")
def test_safeblock(self):
im1 = hopper()
@@ -94,12 +107,6 @@ class TestImageFile:
assert_image_equal(im1, im2)
- def test_raise_ioerror(self):
- with pytest.raises(IOError):
- with pytest.warns(DeprecationWarning) as record:
- ImageFile.raise_ioerror(1)
- assert len(record) == 1
-
def test_raise_oserror(self):
with pytest.raises(OSError):
ImageFile.raise_oserror(1)
@@ -117,6 +124,20 @@ class TestImageFile:
with pytest.raises(OSError):
p.close()
+ def test_truncated(self):
+ b = BytesIO(
+ b"BM000000000000" # head_data
+ + _binary.o32le(
+ ImageFile.SAFEBLOCK + 1 + 4
+ ) # header_size, so BmpImagePlugin will try to read SAFEBLOCK + 1 bytes
+ + (
+ b"0" * ImageFile.SAFEBLOCK
+ ) # only SAFEBLOCK bytes, so that the header is truncated
+ )
+ with pytest.raises(OSError) as e:
+ BmpImagePlugin.BmpImageFile(b)
+ assert str(e.value) == "Truncated File Read"
+
@skip_unless_feature("zlib")
def test_truncated_with_errors(self):
with Image.open("Tests/images/truncated_image.png") as im:
@@ -248,4 +269,4 @@ class TestPyDecoder:
def test_oserror(self):
im = Image.new("RGB", (1, 1))
with pytest.raises(OSError):
- im.save(BytesIO(), "JPEG2000")
+ im.save(BytesIO(), "JPEG2000", num_resolutions=2)
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index dc88cb31d..0d423aab7 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -131,6 +131,20 @@ class TestImageFont:
target = "Tests/images/transparent_background_text.png"
assert_image_similar_tofile(im, target, 4.09)
+ target = "Tests/images/transparent_background_text_L.png"
+ assert_image_similar_tofile(im.convert("L"), target, 0.01)
+
+ def test_I16(self):
+ im = Image.new(mode="I;16", size=(300, 100))
+ draw = ImageDraw.Draw(im)
+ ttf = self.get_font()
+
+ txt = "Hello World!"
+ draw.text((10, 10), txt, font=ttf)
+
+ target = "Tests/images/transparent_background_text_L.png"
+ assert_image_similar_tofile(im.convert("L"), target, 0.01)
+
def test_textsize_equal(self):
im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im)
@@ -141,7 +155,6 @@ class TestImageFont:
draw.text((10, 10), txt, font=ttf)
draw.rectangle((10, 10, 10 + size[0], 10 + size[1]))
- # Epsilon ~.5 fails with FreeType 2.7
assert_image_similar_tofile(
im, "Tests/images/rectangle_surrounding_text.png", 2.5
)
@@ -202,8 +215,7 @@ class TestImageFont:
draw = ImageDraw.Draw(im)
draw.text((0, 0), TEST_TEXT, font=ttf)
- # Epsilon ~.5 fails with FreeType 2.7
- assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2)
+ assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01)
# Test that text() can pass on additional arguments
# to multiline_text()
@@ -218,9 +230,8 @@ class TestImageFont:
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align)
- # Epsilon ~.5 fails with FreeType 2.7
assert_image_similar_tofile(
- im, "Tests/images/multiline_text" + ext + ".png", 6.2
+ im, "Tests/images/multiline_text" + ext + ".png", 0.01
)
def test_unknown_align(self):
@@ -275,8 +286,7 @@ class TestImageFont:
draw = ImageDraw.Draw(im)
draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10)
- # Epsilon ~.5 fails with FreeType 2.7
- assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 6.2)
+ assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5)
def test_rotated_transposed_font(self):
img_grey = Image.new("L", (100, 100))
@@ -716,31 +726,34 @@ class TestImageFont:
font.set_variation_by_axes([100])
self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
+ def test_textbbox_non_freetypefont(self):
+ im = Image.new("RGB", (200, 200))
+ d = ImageDraw.Draw(im)
+ default_font = ImageFont.load_default()
+ with pytest.raises(ValueError):
+ d.textbbox((0, 0), "test", font=default_font)
+
@pytest.mark.parametrize(
- "anchor, left, left_old, top",
+ "anchor, left, top",
(
# test horizontal anchors
- ("ls", 0, 0, -36),
- ("ms", -64, -65, -36),
- ("rs", -128, -129, -36),
+ ("ls", 0, -36),
+ ("ms", -64, -36),
+ ("rs", -128, -36),
# test vertical anchors
- ("ma", -64, -65, 16),
- ("mt", -64, -65, 0),
- ("mm", -64, -65, -17),
- ("mb", -64, -65, -44),
- ("md", -64, -65, -51),
+ ("ma", -64, 16),
+ ("mt", -64, 0),
+ ("mm", -64, -17),
+ ("mb", -64, -44),
+ ("md", -64, -51),
),
ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"),
)
- def test_anchor(self, anchor, left, left_old, top):
+ def test_anchor(self, anchor, left, top):
name, text = "quick", "Quick"
path = f"Tests/images/test_anchor_{name}_{anchor}.png"
- freetype = parse_version(features.version_module("freetype2"))
- if freetype < parse_version("2.4"):
- width, height = (129, 44)
- left = left_old
- elif self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM:
+ if self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM:
width, height = (129, 44)
else:
width, height = (128, 44)
@@ -846,6 +859,22 @@ class TestImageFont:
assert_image_equal_tofile(im, target)
+ def test_bitmap_font_stroke(self):
+ text = "Bitmap Font"
+ layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE]
+ target = f"Tests/images/bitmap_font_stroke_{layout_name}.png"
+ font = ImageFont.truetype(
+ "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf",
+ 24,
+ layout_engine=self.LAYOUT_ENGINE,
+ )
+
+ im = Image.new("RGB", (160, 35), "white")
+ draw = ImageDraw.Draw(im)
+ draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red")
+
+ assert_image_similar_tofile(im, target, 0.03)
+
def test_standard_embedded_color(self):
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE)
@@ -857,7 +886,6 @@ class TestImageFont:
assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2)
- @skip_unless_feature_version("freetype2", "2.5.0")
def test_cbdt(self):
try:
font = ImageFont.truetype(
@@ -872,11 +900,10 @@ class TestImageFont:
d.text((10, 10), "\U0001f469", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2)
- except IOError as e: # pragma: no cover
+ except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support")
- @skip_unless_feature_version("freetype2", "2.5.0")
def test_cbdt_mask(self):
try:
font = ImageFont.truetype(
@@ -893,11 +920,10 @@ class TestImageFont:
assert_image_similar_tofile(
im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
)
- except IOError as e: # pragma: no cover
+ except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support")
- @skip_unless_feature_version("freetype2", "2.5.1")
def test_sbix(self):
try:
font = ImageFont.truetype(
@@ -912,11 +938,10 @@ class TestImageFont:
d.text((50, 50), "\uE901", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
- except IOError as e: # pragma: no cover
+ except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or SBIX support")
- @skip_unless_feature_version("freetype2", "2.5.1")
def test_sbix_mask(self):
try:
font = ImageFont.truetype(
@@ -931,7 +956,7 @@ class TestImageFont:
d.text((50, 50), "\uE901", (100, 0, 0), font=font)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
- except IOError as e: # pragma: no cover
+ except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or SBIX support")
@@ -971,7 +996,6 @@ class TestImageFont_RaqmLayout(TestImageFont):
LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM
-@skip_unless_feature_version("freetype2", "2.4", "Different metrics")
def test_render_mono_size():
# issue 4177
@@ -987,13 +1011,14 @@ def test_render_mono_size():
assert_image_equal_tofile(im, "Tests/images/text_mono.gif")
-def test_freetype_deprecation(monkeypatch):
- # Arrange: mock features.version_module to return fake FreeType version
- def fake_version_module(module):
- return "2.7"
-
- monkeypatch.setattr(features, "version_module", fake_version_module)
-
- # Act / Assert
- with pytest.warns(DeprecationWarning):
- ImageFont.truetype(FONT_PATH, FONT_SIZE)
+@pytest.mark.parametrize(
+ "test_file",
+ [
+ "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf",
+ ],
+)
+def test_oom(test_file):
+ with open(test_file, "rb") as f:
+ font = ImageFont.truetype(BytesIO(f.read()))
+ with pytest.raises(Image.DecompressionBombError):
+ font.getmask("Test Text")
diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py
index f2a914ff7..ffb70cf17 100644
--- a/Tests/test_imagefontctl.py
+++ b/Tests/test_imagefontctl.py
@@ -1,13 +1,8 @@
import pytest
-from packaging.version import parse as parse_version
-from PIL import Image, ImageDraw, ImageFont, features
+from PIL import Image, ImageDraw, ImageFont
-from .helper import (
- assert_image_similar_tofile,
- skip_unless_feature,
- skip_unless_feature_version,
-)
+from .helper import assert_image_similar_tofile, skip_unless_feature
FONT_SIZE = 20
FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
@@ -252,11 +247,6 @@ def test_getlength_combine(mode, direction, text):
pytest.skip("libraqm 0.7 or greater not available")
-# FreeType 2.5.1 README: Miscellaneous Changes:
-# Improved computation of emulated vertical metrics for TrueType fonts.
-@skip_unless_feature_version(
- "freetype2", "2.5.1", "FreeType <2.5.1 has incompatible ttb metrics"
-)
@pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm"))
def test_anchor_ttb(anchor):
text = "f"
@@ -315,14 +305,6 @@ combine_tests = (
"name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests]
)
def test_combine(name, text, dir, anchor, epsilon):
- if (
- parse_version(features.version_module("freetype2")) < parse_version("2.5.1")
- and dir == "ttb"
- ):
- # FreeType 2.5.1 README: Miscellaneous Changes:
- # Improved computation of emulated vertical metrics for TrueType fonts.
- pytest.skip("FreeType <2.5.1 has incompatible ttb metrics")
-
path = f"Tests/images/test_combine_{name}.png"
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index c36285451..fa2291582 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -6,7 +6,7 @@ import pytest
from PIL import Image, ImageGrab
-from .helper import assert_image, assert_image_equal_tofile, skip_unless_feature
+from .helper import assert_image_equal_tofile, skip_unless_feature
class TestImageGrab:
@@ -14,25 +14,20 @@ class TestImageGrab:
sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
)
def test_grab(self):
- for im in [
- ImageGrab.grab(),
- ImageGrab.grab(include_layered_windows=True),
- ImageGrab.grab(all_screens=True),
- ]:
- assert_image(im, im.mode, im.size)
+ ImageGrab.grab()
+ ImageGrab.grab(include_layered_windows=True)
+ ImageGrab.grab(all_screens=True)
im = ImageGrab.grab(bbox=(10, 20, 50, 80))
- assert_image(im, im.mode, (40, 60))
+ assert im.size == (40, 60)
@skip_unless_feature("xcb")
def test_grab_x11(self):
try:
if sys.platform not in ("win32", "darwin"):
- im = ImageGrab.grab()
- assert_image(im, im.mode, im.size)
+ ImageGrab.grab()
- im2 = ImageGrab.grab(xdisplay="")
- assert_image(im2, im2.mode, im2.size)
+ ImageGrab.grab(xdisplay="")
except OSError as e:
pytest.skip(str(e))
@@ -71,8 +66,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only"
return
- im = ImageGrab.grabclipboard()
- assert_image(im, im.mode, im.size)
+ ImageGrab.grabclipboard()
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self):
diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py
index 239806796..25811aa89 100644
--- a/Tests/test_imagemath.py
+++ b/Tests/test_imagemath.py
@@ -1,9 +1,11 @@
+import pytest
+
from PIL import Image, ImageMath
def pixel(im):
if hasattr(im, "im"):
- return "{} {}".format(im.mode, repr(im.getpixel((0, 0))))
+ return f"{im.mode} {repr(im.getpixel((0, 0)))}"
else:
if isinstance(im, int):
return int(im) # hack to deal with booleans
@@ -50,6 +52,11 @@ def test_ops():
assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0"
+def test_prevent_exec():
+ with pytest.raises(ValueError):
+ ImageMath.eval("exec('pass')")
+
+
def test_logical():
assert pixel(ImageMath.eval("not A", images)) == 0
assert pixel(ImageMath.eval("A and B", images)) == "L 2"
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index eb41d2d08..368c2bba1 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -235,19 +235,19 @@ def test_negate():
)
-def test_non_binary_images():
+def test_incorrect_mode():
im = hopper("RGB")
mop = ImageMorph.MorphOp(op_name="erosion8")
- with pytest.raises(Exception) as e:
+ with pytest.raises(ValueError) as e:
mop.apply(im)
- assert str(e.value) == "Image must be binary, meaning it must use mode L"
- with pytest.raises(Exception) as e:
+ assert str(e.value) == "Image mode must be L"
+ with pytest.raises(ValueError) as e:
mop.match(im)
- assert str(e.value) == "Image must be binary, meaning it must use mode L"
- with pytest.raises(Exception) as e:
+ assert str(e.value) == "Image mode must be L"
+ with pytest.raises(ValueError) as e:
mop.get_on_pixels(im)
- assert str(e.value) == "Image must be binary, meaning it must use mode L"
+ assert str(e.value) == "Image mode must be L"
def test_add_patterns():
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
index 33489bd13..6aa1cf35e 100644
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -29,6 +29,7 @@ def test_sanity():
ImageOps.autocontrast(hopper("L"), cutoff=(2, 10))
ImageOps.autocontrast(hopper("L"), ignore=[0, 255])
ImageOps.autocontrast(hopper("L"), mask=hopper("L"))
+ ImageOps.autocontrast(hopper("L"), preserve_tone=True)
ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255))
ImageOps.colorize(hopper("L"), "black", "white")
@@ -36,6 +37,9 @@ def test_sanity():
ImageOps.pad(hopper("L"), (128, 128))
ImageOps.pad(hopper("RGB"), (128, 128))
+ ImageOps.contain(hopper("L"), (128, 128))
+ ImageOps.contain(hopper("RGB"), (128, 128))
+
ImageOps.crop(hopper("L"), 1)
ImageOps.crop(hopper("RGB"), 1)
@@ -98,6 +102,13 @@ def test_fit_same_ratio():
assert new_im.size == (1000, 755)
+@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
+def test_contain(new_size):
+ im = hopper()
+ new_im = ImageOps.contain(im, new_size)
+ assert new_im.size == (256, 256)
+
+
def test_pad():
# Same ratio
im = hopper()
@@ -145,6 +156,33 @@ def test_scale():
assert newimg.size == (25, 25)
+@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
+def test_expand_palette(border):
+ with Image.open("Tests/images/p_16.tga") as im:
+ im_expanded = ImageOps.expand(im, border, (255, 0, 0))
+
+ if isinstance(border, int):
+ left = top = right = bottom = border
+ else:
+ left, top, right, bottom = border
+ px = im_expanded.convert("RGB").load()
+ for x in range(im_expanded.width):
+ for b in range(top):
+ assert px[x, b] == (255, 0, 0)
+ for b in range(bottom):
+ assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
+ for y in range(im_expanded.height):
+ for b in range(left):
+ assert px[b, y] == (255, 0, 0)
+ for b in range(right):
+ assert px[im_expanded.width - 1 - b, y] == (255, 0, 0)
+
+ im_cropped = im_expanded.crop(
+ (left, top, im_expanded.width - right, im_expanded.height - bottom)
+ )
+ assert_image_equal(im_cropped, im)
+
+
def test_colorize_2color():
# Test the colorizing function with 2-color functionality
@@ -291,6 +329,7 @@ def test_exif_transpose():
else:
assert transposed_im.info["exif"] != original_exif
+ assert 0x0112 in im.getexif()
assert 0x0112 not in transposed_im.getexif()
# Repeat the operation to test that it does not keep transposing
@@ -304,6 +343,28 @@ def test_exif_transpose():
) as orientation_im:
check(orientation_im)
+ # Orientation from "XML:com.adobe.xmp" info key
+ with Image.open("Tests/images/xmp_tags_orientation.png") as im:
+ assert im.getexif()[0x0112] == 3
+
+ transposed_im = ImageOps.exif_transpose(im)
+ assert 0x0112 not in transposed_im.getexif()
+
+ # Orientation from "Raw profile type exif" info key
+ # This test image has been manually hexedited from exif_imagemagick.png
+ # to have a different orientation
+ with Image.open("Tests/images/exif_imagemagick_orientation.png") as im:
+ assert im.getexif()[0x0112] == 3
+
+ transposed_im = ImageOps.exif_transpose(im)
+ assert 0x0112 not in transposed_im.getexif()
+
+ # Orientation set directly on Image.Exif
+ im = hopper()
+ im.getexif()[0x0112] = 3
+ transposed_im = ImageOps.exif_transpose(im)
+ assert 0x0112 not in transposed_im.getexif()
+
def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast
@@ -336,7 +397,7 @@ def test_autocontrast_mask_toy_input():
assert ImageStat.Stat(result_nomask).median == [128]
-def test_auto_contrast_mask_real_input():
+def test_autocontrast_mask_real_input():
# Test the autocontrast with a rectangular mask
with Image.open("Tests/images/iptc.jpg") as img:
@@ -362,3 +423,52 @@ def test_auto_contrast_mask_real_input():
threshold=2,
msg="autocontrast without mask pixel incorrect",
)
+
+
+def test_autocontrast_preserve_tone():
+ def autocontrast(mode, preserve_tone):
+ im = hopper(mode)
+ return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()
+
+ assert autocontrast("RGB", True) != autocontrast("RGB", False)
+ assert autocontrast("L", True) == autocontrast("L", False)
+
+
+def test_autocontrast_preserve_gradient():
+ gradient = Image.linear_gradient("L")
+
+ # test with a grayscale gradient that extends to 0,255.
+ # Should be a noop.
+ out = ImageOps.autocontrast(gradient, cutoff=0, preserve_tone=True)
+
+ assert_image_equal(gradient, out)
+
+ # cutoff the top and bottom
+ # autocontrast should make the first and last histogram entries equal
+ # and, with rounding, should be 10% of the image pixels
+ out = ImageOps.autocontrast(gradient, cutoff=10, preserve_tone=True)
+ hist = out.histogram()
+ assert hist[0] == hist[-1]
+ assert hist[-1] == 256 * round(256 * 0.10)
+
+ # in rgb
+ img = gradient.convert("RGB")
+ out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True)
+ assert_image_equal(img, out)
+
+
+@pytest.mark.parametrize(
+ "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
+)
+def test_autocontrast_preserve_one_color(color):
+ img = Image.new("RGB", (10, 10), color)
+
+ # single color images shouldn't change
+ out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True)
+ assert_image_equal(img, out) # single color, no cutoff
+
+ # even if there is a cutoff
+ out = ImageOps.autocontrast(
+ img, cutoff=10, preserve_tone=True
+ ) # single color 10 cutoff
+ assert_image_equal(img, out)
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index 0ea2472a9..475d249ed 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -2,33 +2,77 @@ import pytest
from PIL import Image, ImagePalette
-from .helper import assert_image_equal_tofile
+from .helper import assert_image_equal, assert_image_equal_tofile
def test_sanity():
- ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
- with pytest.raises(ValueError):
- ImagePalette.ImagePalette("RGB", list(range(256)) * 2)
+ palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
+ assert len(palette.colors) == 256
+
+ with pytest.warns(DeprecationWarning):
+ with pytest.raises(ValueError):
+ ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
+
+
+def test_reload():
+ with Image.open("Tests/images/hopper.gif") as im:
+ original = im.copy()
+ im.palette.dirty = 1
+ assert_image_equal(im.convert("RGB"), original.convert("RGB"))
def test_getcolor():
palette = ImagePalette.ImagePalette()
+ assert len(palette.palette) == 0
+ assert len(palette.colors) == 0
test_map = {}
for i in range(256):
test_map[palette.getcolor((i, i, i))] = i
-
assert len(test_map) == 256
+
+ # Colors can be converted between RGB and RGBA
+ rgba_palette = ImagePalette.ImagePalette("RGBA")
+ assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255))
+
+ assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255))
+
+ # An error is raised when the palette is full
with pytest.raises(ValueError):
palette.getcolor((1, 2, 3))
+ # But not if the image is not using one of the palette entries
+ palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1)))
# Test unknown color specifier
with pytest.raises(ValueError):
palette.getcolor("unknown")
+@pytest.mark.parametrize(
+ "index, palette",
+ [
+ # Test when the palette is not full
+ (0, ImagePalette.ImagePalette()),
+ # Test when the palette is full
+ (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)),
+ ],
+)
+def test_getcolor_not_special(index, palette):
+ im = Image.new("P", (1, 1))
+
+ # Do not use transparency index as a new color
+ im.info["transparency"] = index
+ index1 = palette.getcolor((0, 0, 0), im)
+ assert index1 != index
+
+ # Do not use background index as a new color
+ im.info["background"] = index1
+ index2 = palette.getcolor((0, 0, 1), im)
+ assert index2 not in (index, index1)
+
+
def test_file(tmp_path):
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
@@ -116,7 +160,7 @@ def test_getdata():
mode, data_out = palette.getdata()
# Assert
- assert mode == "RGB;L"
+ assert mode == "RGB"
def test_rawmode_getdata():
diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py
index 0835fdb43..b18271cc5 100644
--- a/Tests/test_imagepath.py
+++ b/Tests/test_imagepath.py
@@ -90,6 +90,8 @@ def test_path_odd_number_of_coordinates():
[
([0, 1, 2, 3], (0.0, 1.0, 2.0, 3.0)),
([3, 2, 1, 0], (1.0, 0.0, 3.0, 2.0)),
+ (0, (0.0, 0.0, 0.0, 0.0)),
+ (1, (0.0, 0.0, 0.0, 0.0)),
],
)
def test_getbbox(coords, expected):
diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py
index 53b1fef7c..589cb5a21 100644
--- a/Tests/test_imageqt.py
+++ b/Tests/test_imageqt.py
@@ -2,7 +2,7 @@ import pytest
from PIL import ImageQt
-from .helper import hopper
+from .helper import assert_image_similar, hopper
pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
@@ -42,8 +42,17 @@ def test_rgb():
def test_image():
- for mode in ("1", "RGB", "RGBA", "L", "P"):
- ImageQt.ImageQt(hopper(mode))
+ modes = ["1", "RGB", "RGBA", "L", "P"]
+ qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage
+ if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
+ modes.append("I;16")
+
+ for mode in modes:
+ im = hopper(mode)
+ roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im))
+ if mode not in ("RGB", "RGBA"):
+ im = im.convert("RGB")
+ assert_image_similar(roundtripped_im, im, 1)
def test_closed_file():
diff --git a/Tests/test_map.py b/Tests/test_map.py
index 752c5f268..42f3447eb 100644
--- a/Tests/test_map.py
+++ b/Tests/test_map.py
@@ -24,11 +24,17 @@ def test_overflow():
def test_tobytes():
+ # Note that this image triggers the decompression bomb warning:
+ max_pixels = Image.MAX_IMAGE_PIXELS
+ Image.MAX_IMAGE_PIXELS = None
+
# Previously raised an access violation on Windows
with Image.open("Tests/images/l2rgb_read.bmp") as im:
with pytest.raises((ValueError, MemoryError, OSError)):
im.tobytes()
+ Image.MAX_IMAGE_PIXELS = max_pixels
+
@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system")
def test_ysize():
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
index a10dcec8c..5fd045855 100644
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -2,9 +2,12 @@ import pickle
import pytest
-from PIL import Image
+from PIL import Image, ImageDraw, ImageFont
-from .helper import skip_unless_feature
+from .helper import assert_image_equal, skip_unless_feature
+
+FONT_SIZE = 20
+FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf"
def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode):
@@ -85,10 +88,55 @@ def test_pickle_la_mode_with_palette(tmp_path):
@skip_unless_feature("webp")
def test_pickle_tell():
# Arrange
- image = Image.open("Tests/images/hopper.webp")
+ with Image.open("Tests/images/hopper.webp") as image:
- # Act: roundtrip
- unpickled_image = pickle.loads(pickle.dumps(image))
+ # Act: roundtrip
+ unpickled_image = pickle.loads(pickle.dumps(image))
# Assert
assert unpickled_image.tell() == 0
+
+
+def helper_assert_pickled_font_images(font1, font2):
+ # Arrange
+ im1 = Image.new(mode="RGBA", size=(300, 100))
+ im2 = Image.new(mode="RGBA", size=(300, 100))
+ draw1 = ImageDraw.Draw(im1)
+ draw2 = ImageDraw.Draw(im2)
+ txt = "Hello World!"
+
+ # Act
+ draw1.text((10, 10), txt, font=font1)
+ draw2.text((10, 10), txt, font=font2)
+
+ # Assert
+ assert_image_equal(im1, im2)
+
+
+@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
+def test_pickle_font_string(protocol):
+ # Arrange
+ font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
+
+ # Act: roundtrip
+ pickled_font = pickle.dumps(font, protocol)
+ unpickled_font = pickle.loads(pickled_font)
+
+ # Assert
+ helper_assert_pickled_font_images(font, unpickled_font)
+
+
+@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
+def test_pickle_font_file(tmp_path, protocol):
+ # Arrange
+ font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
+ filename = str(tmp_path / "temp.pkl")
+
+ # Act: roundtrip
+ with open(filename, "wb") as f:
+ pickle.dump(font, f, protocol)
+ with open(filename, "rb") as f:
+ unpickled_font = pickle.load(f)
+
+ # Assert
+ helper_assert_pickled_font_images(font, unpickled_font)
diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py
index 31f0f493b..e74d79828 100644
--- a/Tests/test_psdraw.py
+++ b/Tests/test_psdraw.py
@@ -1,6 +1,8 @@
import os
import sys
-from io import StringIO
+from io import BytesIO
+
+import pytest
from PIL import Image, PSDraw
@@ -44,10 +46,21 @@ def test_draw_postscript(tmp_path):
assert os.path.getsize(tempfile) > 0
-def test_stdout():
+@pytest.mark.parametrize("buffer", (True, False))
+def test_stdout(buffer):
# Temporarily redirect stdout
old_stdout = sys.stdout
- sys.stdout = mystdout = StringIO()
+
+ if buffer:
+
+ class MyStdOut:
+ buffer = BytesIO()
+
+ mystdout = MyStdOut()
+ else:
+ mystdout = BytesIO()
+
+ sys.stdout = mystdout
ps = PSDraw.PSDraw()
_create_document(ps)
@@ -55,4 +68,6 @@ def test_stdout():
# Reset stdout
sys.stdout = old_stdout
- assert mystdout.getvalue() != ""
+ if buffer:
+ mystdout = mystdout.buffer
+ assert mystdout.getvalue() != b""
diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py
index d4ddc12f9..b5f9d4424 100644
--- a/Tests/test_sgi_crash.py
+++ b/Tests/test_sgi_crash.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
import pytest
from PIL import Image
@@ -22,6 +21,6 @@ from PIL import Image
)
def test_crashes(test_file):
with open(test_file, "rb") as f:
- im = Image.open(f)
- with pytest.raises(OSError):
- im.load()
+ with Image.open(f) as im:
+ with pytest.raises(OSError):
+ im.load()
diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py
index ae4d0f100..143765b8e 100644
--- a/Tests/test_tiff_crashes.py
+++ b/Tests/test_tiff_crashes.py
@@ -33,10 +33,14 @@ from .helper import on_ci
"Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif",
"Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif",
"Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif",
+ "Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif",
+ "Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif",
+ "Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif",
],
)
@pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data")
@pytest.mark.filterwarnings("ignore:Metadata warning")
+@pytest.mark.filterwarnings("ignore:Truncated File Read")
def test_tiff_crashes(test_file):
try:
with Image.open(test_file) as im:
diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py
index 1697a8d49..12f475df0 100644
--- a/Tests/test_tiff_ifdrational.py
+++ b/Tests/test_tiff_ifdrational.py
@@ -28,6 +28,8 @@ def test_sanity():
_test_equal(1, 2, Fraction(1, 2))
_test_equal(1, 2, IFDRational(1, 2))
+ _test_equal(7, 5, 1.4)
+
def test_ranges():
for num in range(1, 10):
diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh
index 376d8ef9b..774f26767 100755
--- a/depends/install_imagequant.sh
+++ b/depends/install_imagequant.sh
@@ -1,9 +1,9 @@
#!/bin/bash
# install libimagequant
-archive=libimagequant-2.14.1
+archive=libimagequant-2.17.0
-./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
+./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh
index 7321b80f0..914e71e53 100755
--- a/depends/install_openjpeg.sh
+++ b/depends/install_openjpeg.sh
@@ -3,7 +3,7 @@
archive=openjpeg-2.4.0
-./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
+./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh
index a7ce16792..3105465ec 100755
--- a/depends/install_raqm.sh
+++ b/depends/install_raqm.sh
@@ -4,7 +4,7 @@
archive=raqm-0.7.1
-./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
+./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh
index c0dcd93b7..7d2c399df 100755
--- a/depends/install_raqm_cmake.sh
+++ b/depends/install_raqm_cmake.sh
@@ -4,7 +4,7 @@
archive=raqm-cmake-99300ff3
-./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
+./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
diff --git a/depends/install_webp.sh b/depends/install_webp.sh
index 568cb2df9..8a9c96804 100755
--- a/depends/install_webp.sh
+++ b/depends/install_webp.sh
@@ -1,9 +1,9 @@
#!/bin/bash
# install webp
-archive=libwebp-1.2.0
+archive=libwebp-1.2.1
-./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
+./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
pushd $archive
diff --git a/docs/COPYING b/docs/COPYING
index f2466d659..25f03b343 100644
--- a/docs/COPYING
+++ b/docs/COPYING
@@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
- Copyright © 2010-2021 by Alex Clark and contributors
+ Copyright © 2010-2022 by Alex Clark and contributors
Like PIL, Pillow is licensed under the open source PIL
Software License:
diff --git a/docs/Guardfile b/docs/Guardfile
index 16f731611..b689b079a 100755
--- a/docs/Guardfile
+++ b/docs/Guardfile
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
from livereload.compiler import shell
from livereload.task import Task
diff --git a/docs/about.rst b/docs/about.rst
index 51b583ea0..96885d08d 100644
--- a/docs/about.rst
+++ b/docs/about.rst
@@ -19,7 +19,7 @@ The fork author's goal is to foster and support active development of PIL throug
License
-------
-Like PIL, Pillow is `licensed under the open source HPND License `_
+Like PIL, Pillow is `licensed under the open source HPND License `_
Why a fork?
-----------
diff --git a/docs/conf.py b/docs/conf.py
index 123e93c9b..7bbe8c4c9 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -29,11 +29,13 @@ needs_sphinx = "2.4"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
+ "sphinx_copybutton",
+ "sphinx_issues",
+ "sphinx_removed_in",
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
- "sphinx_issues",
- "sphinx_removed_in",
+ "sphinxext.opengraph",
]
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
@@ -51,7 +53,7 @@ master_doc = "index"
# General information about the project.
project = "Pillow (PIL Fork)"
-copyright = "1995-2011 Fredrik Lundh, 2010-2021 Alex Clark and Contributors"
+copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors"
author = "Fredrik Lundh, Alex Clark and Contributors"
# The version info for the project you're documenting, acts as replacement for
@@ -310,9 +312,17 @@ texinfo_documents = [
def setup(app):
app.add_js_file("js/script.js")
+ app.add_css_file("css/styles.css")
app.add_css_file("css/dark.css")
app.add_css_file("css/light.css")
# GitHub repo for sphinx-issues
issues_github_path = "python-pillow/Pillow"
+
+# sphinxext.opengraph
+ogp_image = (
+ "https://raw.githubusercontent.com/python-pillow/pillow-logo/main/"
+ "pillow-logo-dark-text-1280x640.png"
+)
+ogp_image_alt = "Pillow"
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index ef88afa23..ce30fdf3b 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -12,25 +12,12 @@ Deprecated features
Below are features which are considered deprecated. Where appropriate,
a ``DeprecationWarning`` is issued.
-FreeType 2.7
-~~~~~~~~~~~~
-
-.. deprecated:: 8.1.0
-
-Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022-01-02),
-when FreeType 2.8 will be the minimum supported.
-
-We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe
-vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
-
-.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/
-
Tk/Tcl 8.4
~~~~~~~~~~
.. deprecated:: 8.2.0
-Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02),
+Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01),
when Tk/Tcl 8.5 will be the minimum supported.
Categories
@@ -38,27 +25,66 @@ Categories
.. deprecated:: 8.2.0
-``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02),
+``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01),
along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and
``Image.CONTAINER`` attributes.
To determine if an image has multiple frames or not,
``getattr(im, "is_animated", False)`` can be used instead.
+JpegImagePlugin.convert_dict_qtables
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 8.3.0
+
+JPEG ``quantization`` is now automatically converted, but still returned as a
+dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer
+performs any operations on the data given to it, has been deprecated and will be
+removed in Pillow 10.0.0 (2023-07-01).
+
+ImagePalette size parameter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 8.4.0
+
+The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01).
+
+Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
+default, and the size parameter could be used to override that. Pillow 8.3.0 removed
+the default required length, also removing the need for the size parameter.
+
+Removed features
+----------------
+
+Deprecated features are only removed in major releases after an appropriate
+period of deprecation has passed.
+
+PILLOW_VERSION constant
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. deprecated:: 5.2.0
+.. versionremoved:: 9.0.0
+
+Use ``__version__`` instead.
+
+It was initially removed in Pillow 7.0.0, but temporarily brought back in 7.1.0
+to give projects more time to upgrade.
+
Image.show command parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2.0
+.. versionremoved:: 9.0.0
-The ``command`` parameter will be removed in Pillow 9.0.0 (2022-01-02).
-Use a subclass of :py:class:`.ImageShow.Viewer` instead.
+The ``command`` parameter has been removed. Use a subclass of
+:py:class:`.ImageShow.Viewer` instead.
Image._showxv
~~~~~~~~~~~~~
.. deprecated:: 7.2.0
+.. versionremoved:: 9.0.0
-``Image._showxv`` will be removed in Pillow 9.0.0 (2022-01-02).
Use :py:meth:`.Image.Image.show` instead. If custom behaviour is required, use
:py:func:`.ImageShow.register` to add a custom :py:class:`.ImageShow.Viewer` class.
@@ -66,27 +92,24 @@ ImageFile.raise_ioerror
~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2.0
+.. versionremoved:: 9.0.0
``IOError`` was merged into ``OSError`` in Python 3.3.
-So, ``ImageFile.raise_ioerror`` will be removed in Pillow 9.0.0 (2022-01-02).
+So, ``ImageFile.raise_ioerror`` has been removed.
Use ``ImageFile.raise_oserror`` instead.
-PILLOW_VERSION constant
-~~~~~~~~~~~~~~~~~~~~~~~
+FreeType 2.7
+~~~~~~~~~~~~
-.. deprecated:: 5.2.0
+.. deprecated:: 8.1.0
+.. versionremoved:: 9.0.0
-``PILLOW_VERSION`` will be removed in Pillow 9.0.0 (2022-01-02).
-Use ``__version__`` instead.
+Support for FreeType 2.7 has been removed.
-It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects
-more time to upgrade.
+We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe
+vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
-Removed features
-----------------
-
-Deprecated features are only removed in major releases after an appropriate
-period of deprecation has passed.
+.. _FreeType: https://www.freetype.org
im.offset
~~~~~~~~~
@@ -125,7 +148,6 @@ Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6
they issued a ``DeprecationWarning``:
======================== ===================================================
-
Removed Use instead
======================== ===================================================
``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space`
@@ -251,7 +273,7 @@ PIL.OleFileIO
.. deprecated:: 4.0.0
.. versionremoved:: 6.0.0
-PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of
+PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
PyPI (eg. ``python3 -m pip install olefile``).
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index 78aa3ce72..272409416 100644
--- a/docs/example/DdsImagePlugin.py
+++ b/docs/example/DdsImagePlugin.py
@@ -269,9 +269,9 @@ Image.register_decoder("DXT1", DXT1Decoder)
Image.register_decoder("DXT5", DXT5Decoder)
-def _validate(prefix):
+def _accept(prefix):
return prefix[:4] == b"DDS "
-Image.register_open(DdsImageFile.format, DdsImageFile, _validate)
+Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
Image.register_extension(DdsImageFile.format, ".dds")
diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst
index c4cdda78d..66eeaf6f8 100644
--- a/docs/handbook/concepts.rst
+++ b/docs/handbook/concepts.rst
@@ -44,7 +44,7 @@ supports the following standard modes:
* ``I`` (32-bit signed integer pixels)
* ``F`` (32-bit floating point pixels)
-Pillow also provides limited support for a few special modes, including:
+Pillow also provides limited support for a few additional modes, including:
* ``LA`` (L with alpha)
* ``PA`` (P with alpha)
diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst
index c67f8fb8f..bd44f63a3 100644
--- a/docs/handbook/image-file-formats.rst
+++ b/docs/handbook/image-file-formats.rst
@@ -13,6 +13,14 @@ contents, not their names, but the :py:meth:`~PIL.Image.Image.save` method
looks at the name to determine which format to use, unless the format is given
explicitly.
+When an image is opened from a file, only that instance of the image is considered to
+have the format. Copies of the image will contain data loaded from the file, but not
+the file itself, meaning that it can no longer be considered to be in the original
+format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
+internally creates a copy of the image, the ``fp`` (file pointer), along with any
+methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format`
+attribute will be ``None``.
+
Fully supported formats
-----------------------
@@ -31,6 +39,13 @@ The :py:meth:`~PIL.Image.open` method sets the following
**compression**
Set to ``bmp_rle`` if the file is run-length encoded.
+DDS
+^^^
+
+DDS is a popular container texture format used in video games and natively supported
+by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1,
+DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode.
+
DIB
^^^
@@ -51,7 +66,7 @@ than leaving them in the original color space. The EPS driver can write images
in ``L``, ``RGB`` and ``CMYK`` modes.
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
-method with the following parameter to affect how Ghostscript renders the EPS
+method with the following parameters to affect how Ghostscript renders the EPS
**scale**
Affects the scale of the resultant rasterized image. If the EPS suggests
@@ -60,9 +75,14 @@ method with the following parameter to affect how Ghostscript renders the EPS
relative position of the bounding box is maintained::
im = Image.open(...)
- im.size #(100,100)
+ im.size # (100,100)
im.load(scale=2)
- im.size #(200,200)
+ im.size # (200,200)
+
+**transparency**
+ If true, generates an RGBA image with a transparent background, instead of
+ the default behaviour of an RGB image with a white background.
+
GIF
^^^
@@ -71,8 +91,9 @@ Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
writes run-length encoded files in GIF87a by default, unless GIF89a features
are used or GIF89a is already in use.
-Note that GIF files are always read as grayscale (``L``)
-or palette mode (``P``) images.
+GIF files are initially read as grayscale (``L``) or palette mode (``P``)
+images, but seeking to later frames in an image will change the mode to either
+``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties:
@@ -200,12 +221,16 @@ attributes before loading the file::
ICNS
^^^^
-Pillow reads and (macOS only) writes macOS ``.icns`` files. By default, the
+Pillow reads and writes macOS ``.icns`` files. By default, the
largest available icon is read, though you can override this by setting the
:py:attr:`~PIL.Image.Image.size` property before calling
:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method
sets the following :py:attr:`~PIL.Image.Image.info` property:
+.. note::
+
+ Prior to version 8.3.0, Pillow could only write ICNS files on macOS.
+
**sizes**
A list of supported sizes found in this icon file; these are a
3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina
@@ -247,6 +272,12 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.1.0
+**bitmap_format**
+ By default, the image data will be saved in PNG format. With a bitmap format of
+ "bmp", image data will be saved in BMP format instead.
+
+ .. versionadded:: 8.3.0
+
IM
^^
@@ -730,7 +761,7 @@ The :py:meth:`~PIL.Image.open` method sets the following attributes:
A convenience method, :py:meth:`~PIL.SpiderImagePlugin.SpiderImageFile.convert2byte`,
is provided for converting floating point data to byte data (mode ``L``)::
- im = Image.open('image001.spi').convert2byte()
+ im = Image.open("image001.spi").convert2byte()
Writing files in SPIDER format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -814,7 +845,7 @@ Reading Multi-frame TIFF Images
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers
within the image file. You can combine these methods to seek to the next frame
-(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.num_frames - 1``,
+(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.n_frames - 1``,
and can be accessed in any order.
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
@@ -873,6 +904,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
require a matching type in
:py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` tagtype.
+**exif**
+ Alternate keyword to "tiffinfo", for consistency with other formats.
+
+ .. versionadded:: 8.4.0
+
**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression
@@ -940,7 +976,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
files compared to the slowest, but best, 100.
**method**
- Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0.
+ Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4.
**icc_profile**
The ICC Profile to include in the saved file. Only supported if
@@ -1028,17 +1064,6 @@ is commonly used in fax applications. The DCX decoder can read files containing
When the file is opened, only the first image is read. You can use
:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
-
-DDS
-^^^
-
-DDS is a popular container texture format used in video games and natively
-supported by DirectX.
-Currently, uncompressed RGB data and DXT1, DXT3, and DXT5 pixel formats are
-supported, and only in ``RGBA`` mode.
-
-.. versionadded:: 3.4.0 DXT3
-
FLI, FLC
^^^^^^^^
@@ -1177,6 +1202,7 @@ dpi. To load it at another resolution:
.. code-block:: python
from PIL import Image
+
with Image.open("drawing.wmf") as im:
im.load(dpi=144)
@@ -1188,15 +1214,19 @@ To add other read or write support, use
from PIL import Image
from PIL import WmfImagePlugin
+
class WmfHandler:
def open(self, im):
...
+
def load(self, im):
...
return image
+
def save(self, im, fp, filename):
...
+
wmf_handler = WmfHandler()
WmfImagePlugin.register_handler(wmf_handler)
@@ -1242,8 +1272,10 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 3.0.0
**append_images**
- A list of images to append as additional pages. Each of the
- images in the list can be single or multiframe images.
+ A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each
+ of the images in the list can be single or multiframe images. The ``save_all``
+ parameter must be present and set to ``True`` in conjunction with
+ ``append_images``.
.. versionadded:: 4.2.0
diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst
index 6b68a0562..aa9efe192 100644
--- a/docs/handbook/tutorial.rst
+++ b/docs/handbook/tutorial.rst
@@ -176,12 +176,13 @@ Rolling an image
xsize, ysize = image.size
delta = delta % xsize
- if delta == 0: return image
+ if delta == 0:
+ return image
part1 = image.crop((0, 0, delta, ysize))
part2 = image.crop((delta, 0, xsize, ysize))
- image.paste(part1, (xsize-delta, 0, xsize, ysize))
- image.paste(part2, (0, 0, xsize-delta, ysize))
+ image.paste(part1, (xsize - delta, 0, xsize, ysize))
+ image.paste(part2, (0, 0, xsize - delta, ysize))
return image
@@ -264,6 +265,7 @@ Converting between modes
::
from PIL import Image
+
with Image.open("hopper.ppm") as im:
im = im.convert("L")
@@ -382,14 +384,14 @@ Reading sequences
from PIL import Image
with Image.open("animation.gif") as im:
- im.seek(1) # skip to the second frame
+ im.seek(1) # skip to the second frame
try:
while 1:
- im.seek(im.tell()+1)
+ im.seek(im.tell() + 1)
# do something to im
except EOFError:
- pass # end of sequence
+ pass # end of sequence
As seen in this example, you’ll get an :py:exc:`EOFError` exception when the
sequence ends.
@@ -422,9 +424,9 @@ Drawing PostScript
with Image.open("hopper.ppm") as im:
title = "hopper"
- box = (1*72, 2*72, 7*72, 10*72) # in points
+ box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points
- ps = PSDraw.PSDraw() # default is sys.stdout
+ ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer
ps.begin_document(title)
# draw the image (75 dpi)
@@ -433,7 +435,7 @@ Drawing PostScript
# draw title
ps.setfont("HelveticaNarrow-Bold", 36)
- ps.text((3*72, 4*72), title)
+ ps.text((3 * 72, 4 * 72), title)
ps.end_document()
@@ -462,6 +464,7 @@ Reading from an open file
::
from PIL import Image
+
with open("hopper.ppm", "rb") as fp:
im = Image.open(fp)
@@ -475,6 +478,7 @@ Reading from binary data
from PIL import Image
import io
+
im = Image.open(io.BytesIO(buffer))
Note that the library rewinds the file (using ``seek(0)``) before reading the
@@ -493,6 +497,43 @@ Reading from a tar archive
fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg")
im = Image.open(fp)
+
+Batch processing
+^^^^^^^^^^^^^^^^
+
+Operations can be applied to multiple image files. For example, all PNG images
+in the current directory can be saved as JPEGs at reduced quality.
+
+::
+
+ import glob
+ from PIL import Image
+
+
+ def compress_image(source_path, dest_path):
+ with Image.open(source_path) as img:
+ if img.mode != "RGB":
+ img = img.convert("RGB")
+ img.save(dest_path, "JPEG", optimize=True, quality=80)
+
+
+ paths = glob.glob("*.png")
+ for path in paths:
+ compress_image(path, path[:-4] + ".jpg")
+
+Since images can also be opened from a ``Path`` from the ``pathlib`` module,
+the example could be modified to use ``pathlib`` instead of the ``glob``
+module.
+
+::
+
+ from pathlib import Path
+
+ paths = Path(".").glob("*.png")
+ for path in paths:
+ compress_image(path, path.stem + ".jpg")
+
+
Controlling the decoder
-----------------------
diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst
index 9b670dba8..f69da9a94 100644
--- a/docs/handbook/writing-your-own-file-decoder.rst
+++ b/docs/handbook/writing-your-own-file-decoder.rst
@@ -87,10 +87,13 @@ true color.
Image.register_open(SpamImageFile.format, SpamImageFile, _accept)
- Image.register_extensions(SpamImageFile.format, [
- ".spam",
- ".spa", # DOS version
- ])
+ Image.register_extensions(
+ SpamImageFile.format,
+ [
+ ".spam",
+ ".spa", # DOS version
+ ],
+ )
The format handler must always set the
@@ -111,6 +114,7 @@ Once the plugin has been imported, it can be used:
from PIL import Image
import SpamImagePlugin
+
with Image.open("hopper.spam") as im:
pass
@@ -163,16 +167,16 @@ TIFF, and many others. To use the raw decoder with the
image = Image.frombytes(
mode, size, data, "raw",
- raw mode, stride, orientation
+ raw_mode, stride, orientation
)
When used in a tile descriptor, the parameter field should look like::
- (raw mode, stride, orientation)
+ (raw_mode, stride, orientation)
The fields are used as follows:
-**raw mode**
+**raw_mode**
The pixel layout used in the file, and is used to properly convert data to
PIL’s internal layout. For a summary of the available formats, see the
table below.
@@ -191,34 +195,34 @@ match PIL’s internal pixel layout. PIL supports a large set of raw modes; for
complete list, see the table in the :file:`Unpack.c` module. The following
table describes some commonly used **raw modes**:
-+-----------+-----------------------------------------------------------------+
-| mode | description |
-+===========+=================================================================+
-| ``1`` | 1-bit bilevel, stored with the leftmost pixel in the most |
-| | significant bit. 0 means black, 1 means white. |
-+-----------+-----------------------------------------------------------------+
-| ``1;I`` | 1-bit inverted bilevel, stored with the leftmost pixel in the |
-| | most significant bit. 0 means white, 1 means black. |
-+-----------+-----------------------------------------------------------------+
-| ``1;R`` | 1-bit reversed bilevel, stored with the leftmost pixel in the |
-| | least significant bit. 0 means black, 1 means white. |
-+-----------+-----------------------------------------------------------------+
-| ``L`` | 8-bit greyscale. 0 means black, 255 means white. |
-+-----------+-----------------------------------------------------------------+
-| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. |
-+-----------+-----------------------------------------------------------------+
-| ``P`` | 8-bit palette-mapped image. |
-+-----------+-----------------------------------------------------------------+
-| ``RGB`` | 24-bit true colour, stored as (red, green, blue). |
-+-----------+-----------------------------------------------------------------+
-| ``BGR`` | 24-bit true colour, stored as (blue, green, red). |
-+-----------+-----------------------------------------------------------------+
-| ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). The pad |
-| | pixels may vary. |
-+-----------+-----------------------------------------------------------------+
-| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, then|
-| | all green pixels, finally all blue pixels). |
-+-----------+-----------------------------------------------------------------+
++-----------+-------------------------------------------------------------------+
+| mode | description |
++===========+===================================================================+
+| ``1`` | | 1-bit bilevel, stored with the leftmost pixel in the most |
+| | | significant bit. 0 means black, 1 means white. |
++-----------+-------------------------------------------------------------------+
+| ``1;I`` | | 1-bit inverted bilevel, stored with the leftmost pixel in the |
+| | | most significant bit. 0 means white, 1 means black. |
++-----------+-------------------------------------------------------------------+
+| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the |
+| | | least significant bit. 0 means black, 1 means white. |
++-----------+-------------------------------------------------------------------+
+| ``L`` | 8-bit greyscale. 0 means black, 255 means white. |
++-----------+-------------------------------------------------------------------+
+| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. |
++-----------+-------------------------------------------------------------------+
+| ``P`` | 8-bit palette-mapped image. |
++-----------+-------------------------------------------------------------------+
+| ``RGB`` | 24-bit true colour, stored as (red, green, blue). |
++-----------+-------------------------------------------------------------------+
+| ``BGR`` | 24-bit true colour, stored as (blue, green, red). |
++-----------+-------------------------------------------------------------------+
+| ``RGBX`` | | 24-bit true colour, stored as (red, green, blue, pad). The pad |
+| | | pixels may vary. |
++-----------+-------------------------------------------------------------------+
+| ``RGB;L`` | | 24-bit true colour, line interleaved (first all red pixels, then|
+| | | all green pixels, finally all blue pixels). |
++-----------+-------------------------------------------------------------------+
Note that for the most common cases, the raw mode is simply the same as the mode.
diff --git a/docs/index.rst b/docs/index.rst
index d2aca4bc4..0e16259f3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -25,15 +25,19 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 1.0 no longer supports "import Image". Please use "from PIL import Image" instead.
+.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead.
-.. warning:: Pillow >= 2.1.0 no longer supports "import _imaging". Please use "from PIL.Image import core as _imaging" instead.
+.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead.
Python Support
--------------
Pillow supports these Python versions.
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| **Python** |**3.9**|**3.8**|**3.7**|**3.6**|**3.5**|**3.4**|**3.3**|**3.2**|**2.7**|**2.6**|**2.5**|**2.4**|
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow >= 8.0 | Yes | Yes | Yes | Yes | | | | | | | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow 7.0 - 7.2 | | Yes | Yes | Yes | Yes | | | | | | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow 6.2.1 - 6.2.2 | | Yes | Yes | Yes | Yes | | | | Yes | | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow 6.0 - 6.2.0 | | | Yes | Yes | Yes | | | | Yes | | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow 5.2 - 5.4 | | | Yes | Yes | Yes | Yes | | | Yes | | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow 5.0 - 5.1 | | | | Yes | Yes | Yes | | | Yes | | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow 4 | | | | Yes | Yes | Yes | Yes | | Yes | | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow 2 - 3 | | | | | Yes | Yes | Yes | Yes | Yes | Yes | | |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
-| Pillow < 2 | | | | | | | | | Yes | Yes | Yes | Yes |
-+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 |
++======================+=====+=====+=====+=====+=====+=====+=====+=====+
+| Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | |
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | |
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes |
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes |
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes |
++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+
++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 |
++==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+
+| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | |
++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | |
++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | |
++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
+| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes |
++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
Basic Installation
------------------
@@ -57,8 +65,9 @@ Windows Installation
We provide Pillow binaries for Windows compiled for the matrix of
supported Pythons in both 32 and 64-bit versions in the wheel format.
-These binaries have all of the optional libraries included except
-for raqm, libimagequant, and libxcb::
+These binaries include support for all optional libraries except
+libimagequant and libxcb. Raqm support requires
+FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
@@ -71,8 +80,8 @@ macOS Installation
We provide binaries for macOS for each of the supported Python
versions in the wheel format. These include support for all optional
-libraries except libimagequant and libxcb. Raqm support requires
-libraqm, fribidi, and harfbuzz to be installed separately::
+libraries except libimagequant. Raqm support requires
+FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
@@ -83,7 +92,7 @@ Linux Installation
We provide binaries for Linux for each of the supported Python
versions in the manylinux wheel format. These include support for all
optional libraries except libimagequant. Raqm support requires
-libraqm, fribidi, and harfbuzz to be installed separately::
+FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
@@ -104,7 +113,7 @@ Pillow can be installed on FreeBSD via the official Ports or Packages systems:
**Packages**::
- pkg install py36-pillow
+ pkg install py38-pillow
.. note::
@@ -153,7 +162,7 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality
- * Pillow has been tested with libtiff versions **3.x** and **4.0-4.1**
+ * Pillow has been tested with libtiff versions **3.x** and **4.0-4.3**
* **libfreetype** provides type related services
@@ -178,7 +187,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization
- * Pillow has been tested with libimagequant **2.6-2.14.1**
+ * Pillow has been tested with libimagequant **2.6-2.17.0**
* Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled.
@@ -191,11 +200,15 @@ Many of Pillow's features require external libraries:
* libraqm depends on the following libraries: FreeType, HarfBuzz,
FriBiDi, make sure that you install them before installing libraqm
if not available as package in your system.
- * setting text direction or font features is not supported without
- libraqm.
- * libraqm is dynamically loaded in Pillow 5.0.0 and above, so support
- is available if all the libraries are installed.
- * Windows support: Raqm is not included in prebuilt wheels
+ * Setting text direction or font features is not supported without libraqm.
+ * Pillow wheels since version 8.2.0 include a modified version of libraqm that
+ loads libfribidi at runtime if it is installed.
+ On Windows this requires compiling FriBiDi and installing ``fribidi.dll``
+ into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs)
+ `_
+ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected).
+ See `Build Options`_ to see how to build this version.
+ * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime.
* **libxcb** provides X11 screengrab support.
@@ -244,6 +257,12 @@ Build Options
an exception if the libraries are not found. Webpmux (WebP metadata)
relies on WebP support. Tcl and Tk also must be used together.
+* Build flags: ``--vendor-raqm --vendor-fribidi``
+ These flags are used to compile a modified version of libraqm and
+ a shim that dynamically loads libfribidi at runtime. These are
+ used to compile the standard Pillow wheels. Compiling libraqm requires
+ a C99-compliant compiler.
+
* Build flag: ``--disable-platform-guessing``. Skips all of the
platform dependent guessing of include and library directories for
automated build systems that configure the proper paths in the
@@ -256,10 +275,6 @@ Build Options
Sample usage::
- MAX_CONCURRENCY=1 python3 setup.py build_ext --enable-[feature] install
-
-or using pip::
-
python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]"
@@ -291,7 +306,7 @@ Now install Pillow with::
or from within the uncompressed source directory::
- python3 setup.py install
+ python3 -m pip install .
Building on Windows
^^^^^^^^^^^^^^^^^^^
@@ -426,41 +441,42 @@ Continuous Integration Targets
These platforms are built and tested for every change.
-+----------------------------------+--------------------------+-----------------------+
-|**Operating system** |**Tested Python versions**|**Tested architecture**|
-+----------------------------------+--------------------------+-----------------------+
-| Alpine | 3.8 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Arch | 3.8 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Amazon Linux 2 | 3.7 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| CentOS 7 | 3.6 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| CentOS 8 | 3.6 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Debian 10 Buster | 3.7 |x86 |
-+----------------------------------+--------------------------+-----------------------+
-| Fedora 32 | 3.8 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Fedora 33 | 3.9 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Ubuntu Linux 16.04 LTS (Xenial) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Ubuntu Linux 18.04 LTS (Bionic) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Ubuntu Linux 20.04 LTS (Focal) | 3.8 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Windows Server 2016 | 3.6 |x86-64 |
-+----------------------------------+--------------------------+-----------------------+
-| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9 |x86, x86-64 |
-| +--------------------------+-----------------------+
-| | PyPy3 |x86 |
-| +--------------------------+-----------------------+
-| | 3.8/MinGW |x86, x86-64 |
-+----------------------------------+--------------------------+-----------------------+
++----------------------------------+----------------------------+---------------------+
+| Operating system | Tested Python versions | Tested architecture |
++==================================+============================+=====================+
+| Alpine | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Amazon Linux 2 | 3.7 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Arch | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| CentOS 7 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| CentOS 8 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| CentOS Stream 8 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Debian 10 Buster | 3.7 | x86 |
++----------------------------------+----------------------------+---------------------+
+| Fedora 34 | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Fedora 35 | 3.10 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
+| +----------------------------+---------------------+
+| | 3.8 | arm64v8, ppc64le, |
+| | | s390x |
++----------------------------------+----------------------------+---------------------+
+| Windows Server 2016 | 3.7 | x86-64 |
++----------------------------------+----------------------------+---------------------+
+| Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 |
+| +----------------------------+---------------------+
+| | 3.9/MinGW | x86, x86-64 |
++----------------------------------+----------------------------+---------------------+
Other Platforms
@@ -473,74 +489,79 @@ These platforms have been reported to work at the versions mentioned.
Contributors please test Pillow on your platform then update this
document and send a pull request.
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-|**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| macOS 11.0 Big Sur | 3.8, 3.9 | 8.1.2 |arm |
-| +------------------------------+--------------------------------+-----------------------+
-| | 3.6, 3.7, 3.8, 3.9 | 8.1.2 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 |
-| +------------------------------+--------------------------------+ +
-| | 3.5 | 7.2.0 | |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
-| +------------------------------+--------------------------------+ +
-| | 2.7 | 6.0.0 | |
-| +------------------------------+--------------------------------+ +
-| | 3.4 | 5.4.1 | |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
-| +------------------------------+--------------------------------+ +
-| | 3.3 | 4.1.0 | |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Redhat Linux 6 | 2.6 | |x86 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| CentOS 6.3 | 2.7, 3.3 | |x86 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Ubuntu Linux 12.04 LTS (Precise) | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
-| | PyPy5.3.1, PyPy3 v2.4.0 | | |
-| +------------------------------+--------------------------------+-----------------------+
-| | 2.7 | 4.3.0 |x86-64 |
-| +------------------------------+--------------------------------+-----------------------+
-| | 2.7, 3.2 | 3.4.1 |ppc |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Windows 10 | 3.7 | 7.1.0 |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
-| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
-+----------------------------------+------------------------------+--------------------------------+-----------------------+
++----------------------------------+---------------------------+------------------+--------------+
+| Operating system | | Tested Python | | Latest tested | | Tested |
+| | | versions | | Pillow version | | processors |
++==================================+===========================+==================+==============+
+| macOS 11.0 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
+| +---------------------------+------------------+--------------+
+| | 3.6, 3.7, 3.8, 3.9, 3.10 | 8.4.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 |
+| +---------------------------+------------------+ |
+| | 3.5 | 7.2.0 | |
++----------------------------------+---------------------------+------------------+--------------+
+| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 |
+| +---------------------------+------------------+ |
+| | 2.7 | 6.0.0 | |
+| +---------------------------+------------------+ |
+| | 3.4 | 5.4.1 | |
++----------------------------------+---------------------------+------------------+--------------+
+| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| macOS 10.12 Sierra | 2.7, 3.4, 3.5, 3.6 | 4.1.1 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Mac OS X 10.11 El Capitan | 2.7, 3.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 |
+| +---------------------------+------------------+ |
+| | 3.3 | 4.1.0 | |
++----------------------------------+---------------------------+------------------+--------------+
+| Mac OS X 10.9 Mavericks | 2.7, 3.2, 3.3, 3.4 | 3.0.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Mac OS X 10.8 Mountain Lion | 2.6, 2.7, 3.2, 3.3 | |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Redhat Linux 6 | 2.6 | |x86 |
++----------------------------------+---------------------------+------------------+--------------+
+| CentOS 6.3 | 2.7, 3.3 | |x86 |
++----------------------------------+---------------------------+------------------+--------------+
+| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |
+| | | PyPy5.3.1, PyPy3 v2.4.0 | | |
+| +---------------------------+------------------+--------------+
+| | 2.7 | 4.3.0 |x86-64 |
+| +---------------------------+------------------+--------------+
+| | 2.7, 3.2 | 3.4.1 |ppc |
++----------------------------------+---------------------------+------------------+--------------+
+| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm |
++----------------------------------+---------------------------+------------------+--------------+
+| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm |
++----------------------------------+---------------------------+------------------+--------------+
+| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm |
+| +---------------------------+------------------+ |
+| | 2.7 | 6.2.2 | |
++----------------------------------+---------------------------+------------------+--------------+
+| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| FreeBSD 10.3 | 2.7, 3.4, 3.5 | 4.2.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| FreeBSD 10.2 | 2.7, 3.4 | 3.1.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Windows 10 | 3.7 | 7.1.0 |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Windows 8.1 Pro | 2.6, 2.7, 3.2, 3.3, 3.4 | 2.4.0 |x86,x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Windows 8 Pro | 2.6, 2.7, 3.2, 3.3, 3.4a3 | 2.2.0 |x86,x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Windows 7 Professional | 3.7 | 7.0.0 |x86,x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
+| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 |
++----------------------------------+---------------------------+------------------+--------------+
Old Versions
------------
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index 37fb5f726..b95d8d591 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -18,6 +18,7 @@ Example: Draw a gray cross over an image
.. code-block:: python
+ import sys
from PIL import Image, ImageDraw
with Image.open("hopper.jpg") as im:
@@ -80,11 +81,12 @@ Example: Draw Partial Opacity Text
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
+
# get an image
with Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") as base:
# make a blank image for the text, initialized to transparent text color
- txt = Image.new("RGBA", base.size, (255,255,255,0))
+ txt = Image.new("RGBA", base.size, (255, 255, 255, 0))
# get a font
fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40)
@@ -92,9 +94,9 @@ Example: Draw Partial Opacity Text
d = ImageDraw.Draw(txt)
# draw text, half opacity
- d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128))
+ d.text((10, 10), "Hello", font=fnt, fill=(255, 255, 255, 128))
# draw text, full opacity
- d.text((10,60), "World", font=fnt, fill=(255,255,255,255))
+ d.text((10, 60), "World", font=fnt, fill=(255, 255, 255, 255))
out = Image.alpha_composite(base, txt)
@@ -116,7 +118,7 @@ Example: Draw Multiline Text
d = ImageDraw.Draw(out)
# draw multiline text
- d.multiline_text((10,10), "Hello\nWorld", font=fnt, fill=(0, 0, 0))
+ d.multiline_text((10, 10), "Hello\nWorld", font=fnt, fill=(0, 0, 0))
out.show()
@@ -241,7 +243,7 @@ Methods
numeric values like ``[x, y, x, y, ...]``.
:param fill: Color to use for the point.
-.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None)
+.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None, width=1)
Draws a polygon.
@@ -251,8 +253,9 @@ Methods
:param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or
numeric values like ``[x, y, x, y, ...]``.
- :param outline: Color to use for the outline.
:param fill: Color to use for the fill.
+ :param outline: Color to use for the outline.
+ :param width: The line width, in pixels.
.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None)
@@ -555,7 +558,9 @@ Methods
.. code-block:: python
- hello = draw.textlength("HelloW", font) - draw.textlength("W", font) # adjusted for kerning
+ hello = draw.textlength("HelloW", font) - draw.textlength(
+ "W", font
+ ) # adjusted for kerning
world = draw.textlength("World", font)
hello_world = hello + world # adjusted for kerning
assert hello_world == draw.textlength("HelloWorld", font) # True
diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst
index 813d325e0..5f718ce19 100644
--- a/docs/reference/ImageFont.rst
+++ b/docs/reference/ImageFont.rst
@@ -9,7 +9,7 @@ this class store bitmap fonts, and are used with the
:py:meth:`PIL.ImageDraw.ImageDraw.text` method.
PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use
-`pilfont.py `_
+`pilfont.py `_
from `pillow-scripts `_ to convert BDF and
PCF font descriptors (X window font formats) to this format.
diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst
index 9a16d6625..d1c43cf60 100644
--- a/docs/reference/ImageOps.rst
+++ b/docs/reference/ImageOps.rst
@@ -12,6 +12,7 @@ only work on L and RGB images.
.. autofunction:: autocontrast
.. autofunction:: colorize
+.. autofunction:: contain
.. autofunction:: pad
.. autofunction:: crop
.. autofunction:: scale
diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst
index f14c1c3a4..72ccfac7d 100644
--- a/docs/reference/ImagePalette.rst
+++ b/docs/reference/ImagePalette.rst
@@ -9,10 +9,6 @@ represent the color palette of palette mapped images.
.. note::
- This module was never well-documented. It hasn't changed since 2001,
- though, so it's probably safe for you to read the source code and puzzle
- out the internals if you need to.
-
The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods,
but they are all marked as "experimental." Read that as you will. The
``[source]`` link is there for a reason.
diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst
index f1fbd90ce..45b50c846 100644
--- a/docs/reference/ImageShow.rst
+++ b/docs/reference/ImageShow.rst
@@ -17,7 +17,9 @@ All default viewers convert the image to be shown to PNG format.
The following viewers may be registered on Unix-based systems, if the given command is found:
+ .. autoclass:: PIL.ImageShow.XDGViewer
.. autoclass:: PIL.ImageShow.DisplayViewer
+ .. autoclass:: PIL.ImageShow.GmDisplayViewer
.. autoclass:: PIL.ImageShow.EogViewer
.. autoclass:: PIL.ImageShow.XVViewer
diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst
index f28e58f86..173a0bcc0 100644
--- a/docs/reference/PixelAccess.rst
+++ b/docs/reference/PixelAccess.rst
@@ -17,11 +17,12 @@ changes it.
.. code-block:: python
from PIL import Image
- with Image.open('hopper.jpg') as im:
+
+ with Image.open("hopper.jpg") as im:
px = im.load()
- print (px[4,4])
- px[4,4] = (0,0,0)
- print (px[4,4])
+ print(px[4, 4])
+ px[4, 4] = (0, 0, 0)
+ print(px[4, 4])
Results in the following::
@@ -32,8 +33,8 @@ Access using negative indexes is also possible.
.. code-block:: python
- px[-1,-1] = (0,0,0)
- print (px[-1,-1])
+ px[-1, -1] = (0, 0, 0)
+ print(px[-1, -1])
diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst
index 486c9fc21..e77944d20 100644
--- a/docs/reference/PyAccess.rst
+++ b/docs/reference/PyAccess.rst
@@ -18,11 +18,12 @@ The following script loads an image, accesses one pixel from it, then changes it
.. code-block:: python
from PIL import Image
- with Image.open('hopper.jpg') as im:
+
+ with Image.open("hopper.jpg") as im:
px = im.load()
- print (px[4,4])
- px[4,4] = (0,0,0)
- print (px[4,4])
+ print(px[4, 4])
+ px[4, 4] = (0, 0, 0)
+ print(px[4, 4])
Results in the following::
@@ -33,8 +34,8 @@ Access using negative indexes is also possible.
.. code-block:: python
- px[-1,-1] = (0,0,0)
- print (px[-1,-1])
+ px[-1, -1] = (0, 0, 0)
+ print(px[-1, -1])
diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst
index 527b9d7bc..2ba95b8a6 100644
--- a/docs/reference/c_extension_debugging.rst
+++ b/docs/reference/c_extension_debugging.rst
@@ -63,6 +63,7 @@ Take your test image, and make a really simple harness.
::
from PIL import Image
+
with Image.open(path) as im:
im.load()
@@ -339,7 +340,7 @@ Take your test image, and make a really simple harness.
(vpy38-dbg) ubuntu@primary:~/Home/tests$ gdb python
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later
+ License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
@@ -348,7 +349,7 @@ Take your test image, and make a really simple harness.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
- .
+ .
For help, type "help".
Type "apropos word" to search for commands related to "word"...
diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst
index ed0ab1a0c..6bfd50588 100644
--- a/docs/reference/open_files.rst
+++ b/docs/reference/open_files.rst
@@ -14,17 +14,17 @@ The following are all equivalent::
import io
import pathlib
- with Image.open('test.jpg') as im:
+ with Image.open("test.jpg") as im:
...
- with Image.open(pathlib.Path('test.jpg')) as im2:
+ with Image.open(pathlib.Path("test.jpg")) as im2:
...
- with open('test.jpg', 'rb') as f:
+ with open("test.jpg", "rb") as f:
im3 = Image.open(f)
...
- with open('test.jpg', 'rb') as f:
+ with open("test.jpg", "rb") as f:
im4 = Image.open(io.BytesIO(f.read()))
...
@@ -47,6 +47,10 @@ Image Lifecycle
memory. The image can now be used independently of the underlying
image file.
+ Any Pillow method that creates a new image instance based on another will
+ internally call ``load()`` on the original image and then read the data.
+ The new image instance will not be associated with the original image file.
+
If a filename or a ``Path`` object was passed to ``Image.open()``, then the
file object was opened by Pillow and is considered to be used exclusively by
Pillow. So if the image is a single-frame image, the file will be closed in
@@ -55,10 +59,16 @@ Image Lifecycle
``Image.Image.seek()`` can load the appropriate frame.
* ``Image.Image.close()`` Closes the file and destroys the core image object.
- This is used in the Pillow context manager support. e.g.::
- with Image.open('test.jpg') as img:
- ... # image operations here.
+ The Pillow context manager will also close the file, but will not destroy
+ the core image object. e.g.:
+
+.. code-block:: python
+
+ with Image.open("test.jpg") as img:
+ img.load()
+ assert img.fp is None
+ img.save("test.png")
The lifecycle of a single-frame image is relatively simple. The file must
@@ -80,13 +90,13 @@ Complications
* After a file has been closed, operations that require file access will fail::
- with open('test.jpg', 'rb') as f:
+ with open("test.jpg", "rb") as f:
im5 = Image.open(f)
- im5.load() # FAILS, closed file
+ im5.load() # FAILS, closed file
- with Image.open('test.jpg') as im6:
+ with Image.open("test.jpg") as im6:
pass
- im6.load() # FAILS, closed file
+ im6.load() # FAILS, closed file
Proposed File Handling
diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst
index 03000528f..660d33164 100644
--- a/docs/releasenotes/2.7.0.rst
+++ b/docs/releasenotes/2.7.0.rst
@@ -14,7 +14,7 @@ Png text chunk size limits
To prevent potential denial of service attacks using compressed text
chunks, there are now limits to the decompressed size of text chunks
decoded from PNG images. If the limits are exceeded when opening a PNG
-image a ``ValueError`` will be raised.
+image a :py:exc:`ValueError` will be raised.
Individual text chunks are limited to
:py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by
diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst
index 28dc8324d..2ff9b3799 100644
--- a/docs/releasenotes/8.0.0.rst
+++ b/docs/releasenotes/8.0.0.rst
@@ -78,7 +78,7 @@ Added a new ``formats`` parameter to :py:func:`.Image.open`:
* A list or tuple of formats to attempt to load the file in.
This can be used to restrict the set of formats checked.
Pass ``None`` to try all supported formats. You can print the set of
- available formats by running ``python -m PIL`` or using
+ available formats by running ``python3 -m PIL`` or using
the :py:func:`PIL.features.pilinfo` function.
ImageOps.autocontrast: add mask parameter
diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst
index 3ef05894d..c902ccf71 100644
--- a/docs/releasenotes/8.2.0.rst
+++ b/docs/releasenotes/8.2.0.rst
@@ -4,22 +4,22 @@
Deprecations
============
-Tk/Tcl 8.4
-^^^^^^^^^^
-
-Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02),
-when Tk/Tcl 8.5 will be the minimum supported.
-
Categories
^^^^^^^^^^
-``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02),
+``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01),
along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and
``Image.CONTAINER`` attributes.
To determine if an image has multiple frames or not,
``getattr(im, "is_animated", False)`` can be used instead.
+Tk/Tcl 8.4
+^^^^^^^^^^
+
+Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01),
+when Tk/Tcl 8.5 will be the minimum supported.
+
API Changes
===========
@@ -45,9 +45,31 @@ This is now consistent with other IFDs, and must be accessed through
These changes only affect :py:meth:`~PIL.Image.Image.getexif`, introduced in Pillow
6.0. The older ``_getexif()`` methods are unaffected.
+Image._MODEINFO
+^^^^^^^^^^^^^^^
+
+This internal dictionary had been deprecated by a comment since PIL, and is now
+removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``,
+``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()``
+can be used.
+
API Additions
=============
+getxmp() for JPEG images
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+A new method has been added to return
+`XMP data `_ for JPEG
+images. It reads the XML data into a dictionary of names and values.
+
+For example::
+
+ >>> from PIL import Image
+ >>> with Image.open("Tests/images/xmp_test.jpg") as im:
+ >>> print(im.getxmp())
+ {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...}
+
ImageDraw.rounded_rectangle
^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -63,6 +85,26 @@ create a circle, but not any other ellipse.
draw = ImageDraw.Draw(im)
draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red")
+ImageOps.autocontrast: preserve_tone
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize
+separate histograms for each color channel, changing the tone of the image. The new
+``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram
+for all channels.
+
+ImageShow.GmDisplayViewer
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If GraphicsMagick is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will
+be registered. It uses GraphicsMagick_, an ImageMagick_ fork, to display images.
+
+The GraphicsMagick based viewer has a lower priority than its ImageMagick
+counterpart. Thus, if both ImageMagick and GraphicsMagick are installed,
+``im.show()`` and :py:func:`.ImageShow.show()` prefer the viewer based on
+ImageMagick, i.e the behaviour stays the same for Pillow users having
+ImageMagick installed.
+
ImageShow.IPythonViewer
^^^^^^^^^^^^^^^^^^^^^^^
@@ -83,16 +125,106 @@ be specified through a keyword argument::
im.save("out.tif", icc_profile=...)
+
Security
========
-TODO
+These were all found with `OSS-Fuzz`_.
+
+:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* For J2k images with multiple bands, it's legal to have different widths for each band,
+ e.g. 1 byte for ``L``, 4 bytes for ``A``.
+* This dates to Pillow 2.4.0.
+
+:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input
+ layers with regard to the size of the data block, this could lead to a
+ denial-of-service on :py:meth:`~PIL.Image.open` prior to
+ :py:meth:`~PIL.Image.Image.load`.
+* This dates to the PIL fork.
+
+:cve:`CVE-2021-28676`: Fix FLI DOS
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* ``FliDecode.c`` did not properly check that the block advance was non-zero,
+ potentially leading to an infinite loop on load.
+* This dates to the PIL fork.
+
+:cve:`CVE-2021-28677`: Fix EPS DOS on _open
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line
+ endings. It accidentally used a quadratic method of accumulating lines while looking
+ for a line ending.
+* A malicious EPS file could use this to perform a denial-of-service of Pillow in the
+ open phase, before an image was accepted for opening.
+* This dates to the PIL fork.
+
+:cve:`CVE-2021-28678`: Fix BLP DOS
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets
+ returned data. This could lead to a denial-of-service where the decoder could be run a
+ large number of times on empty data.
+* This dates to Pillow 5.1.0.
+
+Fix memory DOS in ImageFont
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* A corrupt or specially crafted TTF font could have font metrics that lead to
+ unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check
+ the image size before allocating memory for it.
+* This dates to the PIL fork.
Other Changes
=============
+GIF writer uses LZW encoding
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+GIF files are now written using LZW encoding, which will generate smaller files,
+typically about 70% of the size generated by the older encoder.
+
+The pixel data is encoded using the format specified in the `CompuServe GIF standard
+`_.
+
+The older encoder used a variant of run-length encoding that was compatible but less
+efficient.
+
+GraphicsMagick
+^^^^^^^^^^^^^^
+
+The test suite can now be run on systems which have GraphicsMagick_ but not
+ImageMagick_ installed. If both are installed, the tests prefer ImageMagick.
+
+Libraqm and FriBiDi linking
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The way the libraqm dependency for complex text scripts is linked has been changed:
+
+Source builds will now link against the system version of libraqm at build time
+rather than at runtime by default.
+
+Binary wheels now include a statically linked modified version of libraqm that
+links against FriBiDi at runtime instead. This change is intended to address
+issues with the previous implementation on some platforms. These are created
+by building Pillow with the new build flags ``--vendor-raqm --vendor-fribidi``.
+
+Windows users will now need to install ``fribidi.dll`` (or ``fribidi-0.dll``) only,
+``libraqm.dll`` is no longer used.
+
+See :doc:`installation documentation<../installation>` for more information.
+
PyQt6
^^^^^
Support has been added for PyQt6. If it is installed, it will be used instead of
PySide6, PyQt5 or PySide2.
+
+.. _GraphicsMagick: http://www.graphicsmagick.org/
+.. _ImageMagick: https://imagemagick.org/
+.. _OSS-Fuzz: https://github.com/google/oss-fuzz
diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst
new file mode 100644
index 000000000..0bfead144
--- /dev/null
+++ b/docs/releasenotes/8.3.0.rst
@@ -0,0 +1,113 @@
+8.3.0
+-----
+
+Deprecations
+============
+
+JpegImagePlugin.convert_dict_qtables
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+JPEG ``quantization`` is now automatically converted, but still returned as a
+dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer
+performs any operations on the data given to it, has been deprecated and will be
+removed in Pillow 10.0.0 (2023-07-01).
+
+API Changes
+===========
+
+Changed WebP default "method" value when saving
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Previously, it was 0, for the best speed. The default has now been changed to 4, to
+match WebP's default, for higher quality with still some speed optimisation.
+
+Default resampling filter for special image modes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pillow 7.0 changed the default resampling filter to ``Image.BICUBIC``. However, as this
+is not supported yet for images with a custom number of bits, the default filter for
+those modes has been reverted to ``Image.NEAREST``.
+
+ImageMorph incorrect mode errors
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For ``apply()``, ``match()`` and ``get_on_pixels()``, if the image mode is not L, an
+:py:exc:`Exception` was thrown. This has now been changed to a :py:exc:`ValueError`.
+
+getxmp()
+^^^^^^^^
+
+`XMP data `_ can now be
+returned for PNG and TIFF images, through ``getxmp()`` for each format.
+
+The returned dictionary will start from the base of the XML, meaning that the top level
+should contain an "xmpmeta" key. JPEG's ``getxmp()`` method has also been updated to
+this structure.
+
+TIFF getexif()
+^^^^^^^^^^^^^^
+
+TIFF :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` data can now be accessed
+through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GPS and
+EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and
+``im.getexif().get_ifd(0x8769)`` respectively.
+
+API Additions
+=============
+
+ImageOps.contain
+^^^^^^^^^^^^^^^^
+
+Returns a resized version of the image, set to the maximum width and height within
+``size``, while maintaining the original aspect ratio.
+
+To compare it to other ImageOps methods:
+
+- :py:meth:`~PIL.ImageOps.fit` expands an image until is fills ``size``, cropping the
+ parts of the image that do not fit.
+- :py:meth:`~PIL.ImageOps.pad` expands an image to fill ``size``, without cropping, but
+ instead filling the extra space with ``color``.
+- :py:meth:`~PIL.ImageOps.contain` is similar to :py:meth:`~PIL.ImageOps.pad`, but it
+ does not fill the extra space. Instead, the original aspect ratio is maintained. So
+ unlike the other two methods, it is not guaranteed to return an image of ``size``.
+
+ICO saving: bitmap_format argument
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+By default, Pillow saves ICO files in the PNG format. They can now also be saved in BMP
+format, through the new ``bitmap_format`` argument::
+
+ im.save("out.ico", bitmap_format="bmp")
+
+Security
+========
+
+Buffer overflow
+^^^^^^^^^^^^^^^
+
+This release addresses :cve:`CVE-2021-34552`. PIL since 1.1.4 and Pillow since 1.0
+allowed parameters passed into a convert function to trigger buffer overflow in
+Convert.c.
+
+Parsing XML
+^^^^^^^^^^^
+
+Pillow previously parsed XMP data using Python's ``xml`` module. However, this module
+is not secure.
+
+- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve
+ orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead.
+- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It
+ will now use ``defusedxml`` instead. If the dependency is not present, an empty
+ dictionary will be returned and a warning raised.
+
+Other Changes
+=============
+
+Added DDS BC5 reading and uncompressed saving
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read the BC5 format of DDS images, whether UNORM, SNORM or
+TYPELESS.
+
+Support has also been added to write the uncompressed format of DDS images.
diff --git a/docs/releasenotes/8.3.1.rst b/docs/releasenotes/8.3.1.rst
new file mode 100644
index 000000000..e97070c11
--- /dev/null
+++ b/docs/releasenotes/8.3.1.rst
@@ -0,0 +1,40 @@
+8.3.1
+-----
+
+Fixed regression converting to NumPy arrays
+===========================================
+
+This fixes a regression introduced in 8.3.0 when converting an image to a NumPy array
+with a ``dtype`` argument.
+
+.. code-block:: pycon
+
+ >>> from PIL import Image
+ >>> import numpy
+ >>> im = Image.new("RGB", (100, 100))
+ >>> numpy.array(im, dtype=numpy.float64)
+ Traceback (most recent call last):
+ File "", line 1, in
+ TypeError: __array__() takes 1 positional argument but 2 were given
+ >>>
+
+Catch OSError when checking if destination is sys.stdout
+========================================================
+
+In 8.3.0, a check to see if the destination was ``sys.stdout`` when saving an image was
+updated. This lead to an OSError being raised if the environment restricted access.
+
+The OSError is now silently caught.
+
+Fixed removing orientation in ImageOps.exif_transpose
+=====================================================
+
+In 8.3.0, :py:meth:`~PIL.ImageOps.exif_transpose` was changed to ensure that the
+original image EXIF data was not modified, and the orientation was only removed from
+the modified copy.
+
+However, for certain images the orientation was already missing from the modified
+image, leading to a KeyError.
+
+This error has been resolved, and the copying of metadata to the modified image
+improved.
diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst
new file mode 100644
index 000000000..6b5c759fc
--- /dev/null
+++ b/docs/releasenotes/8.3.2.rst
@@ -0,0 +1,41 @@
+8.3.2
+-----
+
+Security
+========
+
+* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service)
+ in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising
+ :py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0.
+
+* Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c``
+ incorrectly calculated the required read buffer size when copying a chunk, potentially
+ reading six extra bytes off the end of the allocated buffer from the heap. Present
+ since Pillow 7.1.0. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs.
+
+Other Changes
+=============
+
+Python 3.10 wheels
+^^^^^^^^^^^^^^^^^^
+
+Pillow now includes binary wheels for Python 3.10.
+
+The Python 3.10 release candidate was released on 2021-08-03 with the final release due
+2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of
+third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI
+changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure
+Pillow can be used immediately on release day of 3.10.0 final.
+
+Fixed regressions
+^^^^^^^^^^^^^^^^^
+
+* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`).
+
+* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`).
+
+* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`).
+
+.. _OSS-Fuzz: https://github.com/google/oss-fuzz
+.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/
+.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/
diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst
new file mode 100644
index 000000000..9becf9146
--- /dev/null
+++ b/docs/releasenotes/8.4.0.rst
@@ -0,0 +1,53 @@
+8.4.0
+-----
+
+API Changes
+===========
+
+Deprecations
+^^^^^^^^^^^^
+
+ImagePalette size parameter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01).
+
+Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
+default, and the size parameter could be used to override that. Pillow 8.3.0 removed
+the default required length, also removing the need for the size parameter.
+
+API Additions
+=============
+
+Added "transparency" argument for loading EPS images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This new argument switches the Ghostscript device from "ppmraw" to "pngalpha",
+generating an RGBA image with a transparent background instead of an RGB image with a
+white background.
+
+.. code-block:: python
+
+ with Image.open("sample.eps") as im:
+ im.load(transparency=True)
+
+Added WalImageFile class
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:func:`PIL.WalImageFile.open()` previously returned a generic
+:py:class:`PIL.Image.Image` instance. It now returns a dedicated
+:py:class:`PIL.WalImageFile.WalImageFile` class.
+
+Other Changes
+=============
+
+Speed improvement when rotating square images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was
+improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the
+rotate operation allowed for expansion and did not specify a center or post-rotate
+translation.
+
+Since the ``expand`` flag makes no difference for square images though, Pillow now
+uses this faster method for square images without the ``expand`` flag as well.
diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst
new file mode 100644
index 000000000..f2be128bb
--- /dev/null
+++ b/docs/releasenotes/9.0.0.rst
@@ -0,0 +1,170 @@
+9.0.0
+-----
+
+Fredrik Lundh
+=============
+
+This release is dedicated to the memory of Fredrik Lundh, aka Effbot, who died in
+November 2021. Fredrik created PIL in 1995 and he was instrumental in the early
+success of Python.
+
+`Guido wrote `_:
+
+ Fredrik was an early Python contributor (e.g. Elementtree and the 're'
+ module) and his enthusiasm for the language and community were inspiring
+ for all who encountered him or his work. He spent countless hours on
+ comp.lang.python answering questions from newbies and advanced users alike.
+
+ He also co-founded an early Python startup, Secret Labs AB, which among
+ other software released an IDE named PythonWorks. Fredrik also created the
+ Python Imaging Library (PIL) which is still THE way to interact with images
+ in Python, now most often through its Pillow fork. His effbot.org site was
+ a valuable resource for generations of Python users, especially its Tkinter
+ documentation.
+
+Thank you, Fredrik.
+
+Backwards Incompatible Changes
+==============================
+
+Python 3.6
+^^^^^^^^^^
+
+Pillow has dropped support for Python 3.6, which reached end-of-life on 2021-12-23.
+
+PILLOW_VERSION constant
+^^^^^^^^^^^^^^^^^^^^^^^
+
+``PILLOW_VERSION`` has been removed. Use ``__version__`` instead.
+
+FreeType 2.7
+^^^^^^^^^^^^
+
+Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported.
+
+We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe
+vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`).
+
+.. _FreeType: https://www.freetype.org
+
+Image.show command parameter
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``command`` parameter has been removed. Use a subclass of
+:py:class:`PIL.ImageShow.Viewer` instead.
+
+Image._showxv
+^^^^^^^^^^^^^
+
+``Image._showxv`` has been removed. Use :py:meth:`~PIL.Image.Image.show`
+instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add
+a custom :py:class:`~PIL.ImageShow.Viewer` class.
+
+ImageFile.raise_ioerror
+^^^^^^^^^^^^^^^^^^^^^^^
+
+``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror``
+has been removed. Use ``ImageFile.raise_oserror`` instead.
+
+
+API Changes
+===========
+
+Added line width parameter to ImageDraw polygon
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``.
+
+
+API Additions
+=============
+
+ImageShow.XDGViewer
+^^^^^^^^^^^^^^^^^^^
+
+If ``xdg-open`` is present on Linux, this new :py:class:`PIL.ImageShow.Viewer` subclass
+will be registered. It displays images using the application selected by the system.
+
+It is higher in priority than the other default :py:class:`PIL.ImageShow.Viewer`
+instances, so it will be preferred by ``im.show()`` or :py:func:`.ImageShow.show()`.
+
+Added support for "title" argument to DisplayViewer
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added for the "title" argument in
+:py:class:`~PIL.ImageShow.UnixViewer.DisplayViewer`, so that when ``im.show()`` or
+:py:func:`.ImageShow.show()` use the ``display`` command line tool, the "title"
+argument will also now be supported, e.g. ``im.show(title="My Image")`` and
+``ImageShow.show(im, title="My Image")``.
+
+Security
+========
+
+Ensure JpegImagePlugin stops at the end of a truncated file
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``JpegImagePlugin`` may append an EOF marker to the end of a truncated file, so that
+the last segment of the data will still be processed by the decoder.
+
+If the EOF marker is not detected as such however, this could lead to an infinite
+loop where ``JpegImagePlugin`` keeps trying to end the file.
+
+Remove consecutive duplicate tiles that only differ by their offset
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To prevent attempts to slow down loading times for images, if an image has consecutive
+duplicate tiles that only differ by their offset, only load the last tile. Credit to
+Google's `OSS-Fuzz`_ project for finding this issue.
+
+Restrict builtins available to ImageMath.eval
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To limit :py:class:`PIL.ImageMath` to working with images, Pillow will now restrict the
+builtins available to :py:meth:`PIL.ImageMath.eval`. This will help prevent problems
+arising if users evaluate arbitrary expressions, such as
+``ImageMath.eval("exec(exit())")``. CVE TBD
+
+Fixed ImagePath.Path array handling
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+CWE-126 and CWE-665 were found when initializing ``ImagePath.Path``. CVEs TBD
+
+.. _OSS-Fuzz: https://github.com/google/oss-fuzz
+
+Other Changes
+=============
+
+Convert subsequent GIF frames to RGB or RGBA
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Since each frame of a GIF can have up to 256 colors, after the first frame it is
+possible for there to be too many colors to fit in a P mode image. To allow for this,
+seeking to any subsequent GIF frame will now convert the image to RGB or RGBA,
+depending on whether or not the first frame had transparency.
+
+Switched to libjpeg-turbo in macOS and Linux wheels
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to
+libjpeg-turbo. It is a fork of libjpeg, popular for its speed.
+
+Added support for pickling TrueType fonts
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+TrueType fonts may now be pickled and unpickled. For example:
+
+.. code-block:: python
+
+ import pickle
+ from PIL import ImageFont
+
+ font = ImageFont.truetype("arial.ttf", size=30)
+ pickled_font = pickle.dumps(font, protocol=pickle.HIGHEST_PROTOCOL)
+
+ # Later...
+ unpickled_font = pickle.loads(pickled_font)
+
+Added support for additional TGA orientations
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+TGA images with top right or bottom right orientations are now supported.
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 117738675..8d1ad7837 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,11 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 9.0.0
+ 8.4.0
+ 8.3.2
+ 8.3.1
+ 8.3.0
8.2.0
8.1.2
8.1.1
diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst
index bf381114e..f7271ae2b 100644
--- a/docs/releasenotes/template.rst
+++ b/docs/releasenotes/template.rst
@@ -34,6 +34,9 @@ TODO
Security
========
+TODO
+^^^^
+
TODO
Other Changes
diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst
index a8c9fc998..87f2ba422 100644
--- a/docs/releasenotes/versioning.rst
+++ b/docs/releasenotes/versioning.rst
@@ -11,7 +11,7 @@ Pillow follows `Semantic Versioning `_:
2. MINOR version when you add functionality in a backwards compatible manner, and
3. PATCH version when you make backwards compatible bug fixes.
-Quarterly releases ("`Main Release `_")
+Quarterly releases ("`Main Release `_")
bump at least the MINOR version, as new functionality has likely been added in the
prior three months.
@@ -21,10 +21,10 @@ these occur every 12-18 months, guided by
`Python's EOL schedule `_, and
any APIs that have been deprecated for at least a year are removed at the same time.
-PATCH versions ("`Point Release `_"
-or "`Embargoed Release `_")
+PATCH versions ("`Point Release `_"
+or "`Embargoed Release `_")
are for security, installation or critical bug fixes. These are less common as it is
preferred to stick to quarterly releases.
-Between quarterly releases, ".dev0" is appended to the "master" branch, indicating that
+Between quarterly releases, ``.dev0`` is appended to the ``main`` branch, indicating that
this is not a formally released copy.
diff --git a/docs/resources/css/dark.css b/docs/resources/css/dark.css
index cc213d674..8866c07ea 100644
--- a/docs/resources/css/dark.css
+++ b/docs/resources/css/dark.css
@@ -1275,7 +1275,7 @@
.wy-body-for-nav {
background-image: initial;
- background-color: rgb(26, 28, 29);
+ background-color: rgb(24, 26, 27);
}
.wy-nav-side {
@@ -1333,11 +1333,6 @@
color: rgb(152, 143, 129);
}
- .wy-body-for-nav {
- background-image: initial;
- background-color: rgb(26, 28, 29);
- }
-
@media screen and (min-width: 1100px) {
.wy-nav-content-wrap {
background-image: initial;
diff --git a/docs/resources/css/styles.css b/docs/resources/css/styles.css
new file mode 100644
index 000000000..111f84085
--- /dev/null
+++ b/docs/resources/css/styles.css
@@ -0,0 +1,8 @@
+th p {
+ margin-bottom: 0;
+}
+
+.rst-content tr .line-block {
+ font-size: 1rem;
+ margin-bottom: 0;
+}
diff --git a/requirements.txt b/requirements.txt
index 4b534ae53..feaa2f718 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,7 @@
black
check-manifest
coverage
+defusedxml
markdown2
olefile
packaging
@@ -10,6 +11,8 @@ pytest
pytest-cov
pytest-timeout
sphinx>=2.4
+sphinx-copybutton
sphinx-issues
sphinx-removed-in
-sphinx-rtd-theme
+sphinx-rtd-theme>=1.0
+sphinxext-opengraph
diff --git a/selftest.py b/selftest.py
index 7e08d183b..4ebd7cc00 100755
--- a/selftest.py
+++ b/selftest.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# minimal sanity check
import sys
@@ -14,17 +14,13 @@ except AttributeError:
pass
-def _info(im):
- im.load()
- return im.format, im.mode, im.size
-
-
def testimage():
"""
PIL lets you create in-memory images with various pixel types:
>>> from PIL import Image, ImageDraw, ImageFilter, ImageMath
>>> im = Image.new("1", (128, 128)) # monochrome
+ >>> def _info(im): return (im.format, im.mode, im.size)
>>> _info(im)
(None, '1', (128, 128))
>>> _info(Image.new("L", (128, 128))) # grayscale (luminance)
diff --git a/setup.cfg b/setup.cfg
index 129adeee7..c3b5a3197 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,40 @@
+[metadata]
+name = Pillow
+description = Python Imaging Library (Fork)
+long_description = file: README.md
+long_description_content_type = text/markdown
+url = https://python-pillow.org
+author = Alex Clark (PIL Fork Author)
+author_email = aclark@python-pillow.org
+license = HPND
+classifiers =
+ Development Status :: 6 - Mature
+ License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3 :: Only
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
+ Programming Language :: Python :: Implementation :: CPython
+ Programming Language :: Python :: Implementation :: PyPy
+ Topic :: Multimedia :: Graphics
+ Topic :: Multimedia :: Graphics :: Capture :: Digital Camera
+ Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
+ Topic :: Multimedia :: Graphics :: Graphics Conversion
+ Topic :: Multimedia :: Graphics :: Viewers
+keywords = Imaging
+project_urls =
+ Documentation=https://pillow.readthedocs.io
+ Source=https://github.com/python-pillow/Pillow
+ Funding=https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi
+ Release notes=https://pillow.readthedocs.io/en/stable/releasenotes/index.html
+ Changelog=https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
+ Twitter=https://twitter.com/PythonPillow
+
+[options]
+python_requires = >=3.7
+
[flake8]
extend-ignore = E203
max-line-length = 88
diff --git a/setup.py b/setup.py
index 6c4840c75..23d91a5f2 100755
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# > pyroma .
# ------------------------------
# Checking .
@@ -26,7 +26,6 @@ def get_version():
return locals()["__version__"]
-NAME = "Pillow"
PILLOW_VERSION = get_version()
FREETYPE_ROOT = None
HARFBUZZ_ROOT = None
@@ -39,7 +38,7 @@ TIFF_ROOT = None
ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
-if sys.platform == "win32" and sys.version_info >= (3, 10):
+if sys.platform == "win32" and sys.version_info >= (3, 11):
import atexit
atexit.register(
@@ -186,7 +185,7 @@ def _find_library_dirs_ldconfig():
return []
[data, _] = p.communicate()
if isinstance(data, bytes):
- data = data.decode()
+ data = data.decode("latin1")
dirs = []
for dll in re.findall(expr, data):
@@ -405,6 +404,27 @@ class pil_build_ext(build_ext):
self.extensions.remove(extension)
break
+ def get_macos_sdk_path(self):
+ try:
+ sdk_path = (
+ subprocess.check_output(["xcrun", "--show-sdk-path"])
+ .strip()
+ .decode("latin1")
+ )
+ except Exception:
+ sdk_path = None
+ if (
+ not sdk_path
+ or sdk_path == "/Applications/Xcode.app/Contents/Developer"
+ "/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"
+ ):
+ commandlinetools_sdk_path = (
+ "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
+ )
+ if os.path.exists(commandlinetools_sdk_path):
+ sdk_path = commandlinetools_sdk_path
+ return sdk_path
+
def build_extensions(self):
library_dirs = []
@@ -532,15 +552,7 @@ class pil_build_ext(build_ext):
_add_directory(library_dirs, "/usr/X11/lib")
_add_directory(include_dirs, "/usr/X11/include")
- # SDK install path
- try:
- sdk_path = (
- subprocess.check_output(["xcrun", "--show-sdk-path"])
- .strip()
- .decode("latin1")
- )
- except Exception:
- sdk_path = None
+ sdk_path = self.get_macos_sdk_path()
if sdk_path:
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
_add_directory(include_dirs, os.path.join(sdk_path, "usr", "include"))
@@ -559,7 +571,11 @@ class pil_build_ext(build_ext):
# headers are at $PREFIX/include
# user libs are at $PREFIX/lib
_add_directory(
- library_dirs, os.path.join(os.environ["ANDROID_ROOT"], "lib")
+ library_dirs,
+ os.path.join(
+ os.environ["ANDROID_ROOT"],
+ "lib" if struct.calcsize("l") == 4 else "lib64",
+ ),
)
elif sys.platform.startswith("netbsd"):
@@ -810,9 +826,11 @@ class pil_build_ext(build_ext):
if feature.tiff:
libs.append(feature.tiff)
defs.append(("HAVE_LIBTIFF", None))
- # FIXME the following define should be detected automatically
- # based on system libtiff, see #4237
- if PLATFORM_MINGW:
+ if sys.platform == "win32":
+ # This define needs to be defined if-and-only-if it was defined
+ # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`,
+ # so we have to guess; by default it is defined in all Windows builds.
+ # See #4237, #5243, #5359 for more information.
defs.append(("USE_WIN32_FILEIO", None))
if feature.xcb:
libs.append(feature.xcb)
@@ -880,7 +898,7 @@ class pil_build_ext(build_ext):
else:
self._remove_extension("PIL._webp")
- tk_libs = ["psapi"] if sys.platform == "win32" else []
+ tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else []
self._update_extension("PIL._imagingtk", tk_libs)
build_ext.build_extensions(self)
@@ -967,54 +985,14 @@ ext_modules = [
Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]),
]
-with open("README.md") as f:
- long_description = f.read()
-
try:
setup(
- name=NAME,
version=PILLOW_VERSION,
- description="Python Imaging Library (Fork)",
- long_description=long_description,
- long_description_content_type="text/markdown",
- license="HPND",
- author="Alex Clark (PIL Fork Author)",
- author_email="aclark@python-pillow.org",
- url="https://python-pillow.org",
- project_urls={
- "Documentation": "https://pillow.readthedocs.io",
- "Source": "https://github.com/python-pillow/Pillow",
- "Funding": "https://tidelift.com/subscription/pkg/pypi-pillow?"
- "utm_source=pypi-pillow&utm_medium=pypi",
- "Release notes": "https://pillow.readthedocs.io/en/stable/releasenotes/"
- "index.html",
- "Changelog": "https://github.com/python-pillow/Pillow/blob/master/"
- "CHANGES.rst",
- },
- classifiers=[
- "Development Status :: 6 - Mature",
- "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
- "Topic :: Multimedia :: Graphics",
- "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
- "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
- "Topic :: Multimedia :: Graphics :: Graphics Conversion",
- "Topic :: Multimedia :: Graphics :: Viewers",
- ],
- python_requires=">=3.6",
cmdclass={"build_ext": pil_build_ext},
ext_modules=ext_modules,
include_package_data=True,
packages=["PIL"],
package_dir={"": "src"},
- keywords=["Imaging"],
zip_safe=not (debug_build() or PLATFORM_MINGW),
)
except RequiredDependencyException as err:
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 88aae80eb..7b78597b4 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -286,33 +286,36 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
raise OSError("Truncated Blp file") from e
return 0, 0
+ def _safe_read(self, length):
+ return ImageFile._safe_read(self.fd, length)
+
def _read_palette(self):
ret = []
for i in range(256):
try:
- b, g, r, a = struct.unpack("<4B", self.fd.read(4))
+ b, g, r, a = struct.unpack("<4B", self._safe_read(4))
except struct.error:
break
ret.append((b, g, r, a))
return ret
def _read_blp_header(self):
- (self._blp_compression,) = struct.unpack("= 52:
for idx, mask in enumerate(
@@ -160,6 +158,8 @@ class BmpImageFile(ImageFile.ImageFile):
if file_info.get("colors", 0)
else (1 << file_info["bits"])
)
+ if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
+ offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
@@ -259,7 +259,7 @@ class BmpImageFile(ImageFile.ImageFile):
]
def _open(self):
- """ Open file, check magic number and read header """
+ """Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index df2d0060c..260924fca 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -14,6 +14,7 @@ import struct
from io import BytesIO
from . import Image, ImageFile
+from ._binary import o32le as o32
# Magic ("DDS ")
DDS_MAGIC = 0x20534444
@@ -97,6 +98,9 @@ DXT5_FOURCC = 0x35545844
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
DXGI_FORMAT_R8G8B8A8_UNORM = 28
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
+DXGI_FORMAT_BC5_TYPELESS = 82
+DXGI_FORMAT_BC5_UNORM = 83
+DXGI_FORMAT_BC5_SNORM = 84
DXGI_FORMAT_BC7_TYPELESS = 97
DXGI_FORMAT_BC7_UNORM = 98
DXGI_FORMAT_BC7_UNORM_SRGB = 99
@@ -127,15 +131,17 @@ class DdsImageFile(ImageFile.ImageFile):
fourcc = header.read(4)
(bitcount,) = struct.unpack("i", file_length))
- if fp_only:
- with open(filename, "rb") as f:
- fp.write(f.read())
+ # TOC
+ fp.write(b"TOC ")
+ fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
+ for entry in entries:
+ fp.write(entry["type"])
+ fp.write(struct.pack(">i", entry["size"]))
+
+ # Data
+ for entry in entries:
+ fp.write(entry["type"])
+ fp.write(struct.pack(">i", entry["size"]))
+ fp.write(entry["stream"])
+
+ if hasattr(fp, "flush"):
+ fp.flush()
-Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns")
+def _accept(prefix):
+ return prefix[:4] == MAGIC
+
+
+Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
Image.register_extension(IcnsImageFile.format, ".icns")
-if sys.platform == "darwin":
- Image.register_save(IcnsImageFile.format, _save)
-
- Image.register_mime(IcnsImageFile.format, "image/icns")
-
+Image.register_save(IcnsImageFile.format, _save)
+Image.register_mime(IcnsImageFile.format, "image/icns")
if __name__ == "__main__":
-
if len(sys.argv) < 2:
- print("Syntax: python IcnsImagePlugin.py [file]")
+ print("Syntax: python3 IcnsImagePlugin.py [file]")
sys.exit()
with open(sys.argv[1], "rb") as fp:
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 5634bf8e9..d9ff9b5e7 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -30,6 +30,7 @@ from math import ceil, log
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16
from ._binary import i32le as i32
+from ._binary import o32le as o32
#
# --------------------------------------------------------------------
@@ -53,6 +54,7 @@ def _save(im, fp, filename):
sizes = list(sizes)
fp.write(struct.pack(".
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index be5efb953..0bafd3907 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -31,58 +31,38 @@ import logging
import math
import numbers
import os
+import re
import struct
import sys
import tempfile
import warnings
-import xml.etree.ElementTree
from collections.abc import Callable, MutableMapping
from pathlib import Path
+try:
+ import defusedxml.ElementTree as ElementTree
+except ImportError:
+ ElementTree = None
+
# VERSION was removed in Pillow 6.0.0.
-# PILLOW_VERSION is deprecated and will be removed in a future release.
+# PILLOW_VERSION was removed in Pillow 9.0.0.
# Use __version__ instead.
-from . import (
- ImageMode,
- TiffTags,
- UnidentifiedImageError,
- __version__,
- _plugins,
- _raise_version_warning,
-)
+from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from ._binary import i32le
from ._util import deferred_error, isPath
-if sys.version_info >= (3, 7):
- def __getattr__(name):
- if name == "PILLOW_VERSION":
- _raise_version_warning()
- return __version__
- else:
- categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
- if name in categories:
- warnings.warn(
- "Image categories are deprecated and will be removed in Pillow 10 "
- "(2023-01-02). Use is_animated instead.",
- DeprecationWarning,
- stacklevel=2,
- )
- return categories[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
-else:
-
- from . import PILLOW_VERSION
-
- # Silence warning
- assert PILLOW_VERSION
-
- # categories
- NORMAL = 0
- SEQUENCE = 1
- CONTAINER = 2
+def __getattr__(name):
+ categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
+ if name in categories:
+ warnings.warn(
+ "Image categories are deprecated and will be removed in Pillow 10 "
+ "(2023-07-01). Use is_animated instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return categories[name]
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
logger = logging.getLogger(__name__)
@@ -158,8 +138,6 @@ def isImageType(t):
#
# Constants
-NONE = 0
-
# transpose
FLIP_LEFT_RIGHT = 0
FLIP_TOP_BOTTOM = 1
@@ -223,28 +201,7 @@ DECODERS = {}
ENCODERS = {}
# --------------------------------------------------------------------
-# Modes supported by this version
-
-_MODEINFO = {
- # NOTE: this table will be removed in future versions. use
- # getmode* functions or ImageMode descriptors instead.
- # official modes
- "1": ("L", "L", ("1",)),
- "L": ("L", "L", ("L",)),
- "I": ("L", "I", ("I",)),
- "F": ("L", "F", ("F",)),
- "P": ("P", "L", ("P",)),
- "RGB": ("RGB", "L", ("R", "G", "B")),
- "RGBX": ("RGB", "L", ("R", "G", "B", "X")),
- "RGBA": ("RGB", "L", ("R", "G", "B", "A")),
- "CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
- "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
- "LAB": ("RGB", "L", ("L", "A", "B")),
- "HSV": ("RGB", "L", ("H", "S", "V")),
- # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and
- # BGR;24. Use these modes only if you know exactly what you're
- # doing...
-}
+# Modes
if sys.byteorder == "little":
_ENDIAN = "<"
@@ -290,7 +247,7 @@ def _conv_type_shape(im):
return (im.size[1], im.size[0], extra), typ
-MODES = sorted(_MODEINFO)
+MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"]
# raw modes that may be memory mapped. NOTE: if you change this, you
# may have to modify the stride calculation in map.c too!
@@ -554,7 +511,7 @@ class Image:
if name == "category":
warnings.warn(
"Image categories are deprecated and will be removed in Pillow 10 "
- "(2023-01-02). Use is_animated instead.",
+ "(2023-07-01). Use is_animated instead.",
DeprecationWarning,
stacklevel=2,
)
@@ -670,7 +627,6 @@ class Image:
and self.size == other.size
and self.info == other.info
and self._category == other._category
- and self.readonly == other.readonly
and self.getpalette() == other.getpalette()
and self.tobytes() == other.tobytes()
)
@@ -697,9 +653,14 @@ class Image:
raise ValueError("Could not save to PNG for display") from e
return b.getvalue()
- @property
- def __array_interface__(self):
+ class _ArrayData:
+ def __init__(self, new):
+ self.__array_interface__ = new
+
+ def __array__(self, dtype=None):
# numpy array interface support
+ import numpy as np
+
new = {}
shape, typestr = _conv_type_shape(self)
new["shape"] = shape
@@ -711,7 +672,8 @@ class Image:
new["data"] = self.tobytes("raw", "L")
else:
new["data"] = self.tobytes()
- return new
+
+ return np.array(self._ArrayData(new), dtype)
def __getstate__(self):
return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()]
@@ -846,10 +808,10 @@ class Image:
arr = bytes(
value for (index, value) in enumerate(arr) if index % 4 != 3
)
- self.im.putpalette(mode, arr)
+ palette_length = self.im.putpalette(mode, arr)
self.palette.dirty = 0
self.palette.rawmode = None
- if "transparency" in self.info:
+ if "transparency" in self.info and mode in ("LA", "PA"):
if isinstance(self.info["transparency"], int):
self.im.putpalettealpha(self.info["transparency"], 0)
else:
@@ -857,6 +819,7 @@ class Image:
self.palette.mode = "RGBA"
else:
self.palette.mode = "RGB"
+ self.palette.palette = self.im.getpalette()[: palette_length * 3]
if self.im:
if cffi and USE_CFFI_ACCESS:
@@ -924,16 +887,18 @@ class Image:
self.load()
+ has_transparency = self.info.get("transparency") is not None
if not mode and self.mode == "P":
# determine default mode
if self.palette:
mode = self.palette.mode
else:
mode = "RGB"
+ if mode == "RGB" and has_transparency:
+ mode = "RGBA"
if not mode or (mode == self.mode and not matrix):
return self.copy()
- has_transparency = self.info.get("transparency") is not None
if matrix:
# matrix conversion
if mode not in ("L", "RGB"):
@@ -951,12 +916,8 @@ class Image:
transparency = convert_transparency(matrix, transparency)
elif len(mode) == 3:
transparency = tuple(
- [
- convert_transparency(
- matrix[i * 4 : i * 4 + 4], transparency
- )
- for i in range(0, len(transparency))
- ]
+ convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
+ for i in range(0, len(transparency))
)
new.info["transparency"] = transparency
return new
@@ -992,23 +953,30 @@ class Image:
if self.mode == "P":
trns_im.putpalette(self.palette)
if isinstance(t, tuple):
+ err = "Couldn't allocate a palette color for transparency"
try:
- t = trns_im.palette.getcolor(t)
- except Exception as e:
- raise ValueError(
- "Couldn't allocate a palette color for transparency"
- ) from e
- trns_im.putpixel((0, 0), t)
-
- if mode in ("L", "RGB"):
- trns_im = trns_im.convert(mode)
+ t = trns_im.palette.getcolor(t, self)
+ except ValueError as e:
+ if str(e) == "cannot allocate more than 256 colors":
+ # If all 256 colors are in use,
+ # then there is no need for transparency
+ t = None
+ else:
+ raise ValueError(err) from e
+ if t is None:
+ trns = None
else:
- # can't just retrieve the palette number, got to do it
- # after quantization.
- trns_im = trns_im.convert("RGB")
- trns = trns_im.getpixel((0, 0))
+ trns_im.putpixel((0, 0), t)
- elif self.mode == "P" and mode == "RGBA":
+ if mode in ("L", "RGB"):
+ trns_im = trns_im.convert(mode)
+ else:
+ # can't just retrieve the palette number, got to do it
+ # after quantization.
+ trns_im = trns_im.convert("RGB")
+ trns = trns_im.getpixel((0, 0))
+
+ elif self.mode == "P" and mode in ("LA", "PA", "RGBA"):
t = self.info["transparency"]
delete_trns = True
@@ -1024,14 +992,14 @@ class Image:
new = self._new(im)
from . import ImagePalette
- new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB"))
+ new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB"))
if delete_trns:
# This could possibly happen if we requantize to fewer colors.
# The transparency would be totally off in that case.
del new.info["transparency"]
if trns is not None:
try:
- new.info["transparency"] = new.palette.getcolor(trns)
+ new.info["transparency"] = new.palette.getcolor(trns, new)
except Exception:
# if we can't make a transparent color, don't leave the old
# transparency hanging around to mess us up.
@@ -1054,16 +1022,25 @@ class Image:
raise ValueError("illegal conversion") from e
new_im = self._new(im)
+ if mode == "P" and palette != ADAPTIVE:
+ from . import ImagePalette
+
+ new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
if delete_trns:
# crash fail if we leave a bytes transparency in an rgb/l mode.
del new_im.info["transparency"]
if trns is not None:
if new_im.mode == "P":
try:
- new_im.info["transparency"] = new_im.palette.getcolor(trns)
- except Exception:
+ new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im)
+ except ValueError as e:
del new_im.info["transparency"]
- warnings.warn("Couldn't allocate palette entry for transparency")
+ if str(e) != "cannot allocate more than 256 colors":
+ # If all 256 colors are in use,
+ # then there is no need for transparency
+ warnings.warn(
+ "Couldn't allocate palette entry for transparency"
+ )
else:
new_im.info["transparency"] = trns
return new_im
@@ -1122,14 +1099,17 @@ class Image:
"only RGB or L mode images can be quantized to a palette"
)
im = self.im.convert("P", dither, palette.im)
- return self._new(im)
+ new_im = self._new(im)
+ new_im.palette = palette.palette.copy()
+ return new_im
im = self._new(self.im.quantize(colors, method, kmeans))
from . import ImagePalette
mode = im.im.getpalettemode()
- im.palette = ImagePalette.ImagePalette(mode, im.im.getpalette(mode, mode))
+ palette = im.im.getpalette(mode, mode)[: colors * len(mode)]
+ im.palette = ImagePalette.ImagePalette(mode, palette)
return im
@@ -1333,30 +1313,60 @@ class Image:
return tuple(extrema)
return self.im.getextrema()
+ def _getxmp(self, xmp_tags):
+ def get_name(tag):
+ return tag.split("}")[1]
+
+ def get_value(element):
+ value = {get_name(k): v for k, v in element.attrib.items()}
+ children = list(element)
+ if children:
+ for child in children:
+ name = get_name(child.tag)
+ child_value = get_value(child)
+ if name in value:
+ if not isinstance(value[name], list):
+ value[name] = [value[name]]
+ value[name].append(child_value)
+ else:
+ value[name] = child_value
+ elif value:
+ if element.text:
+ value["text"] = element.text
+ else:
+ return element.text
+ return value
+
+ if ElementTree is None:
+ warnings.warn("XMP data cannot be read without defusedxml dependency")
+ return {}
+ else:
+ root = ElementTree.fromstring(xmp_tags)
+ return {get_name(root.tag): get_value(root)}
+
def getexif(self):
if self._exif is None:
self._exif = Exif()
exif_info = self.info.get("exif")
- if exif_info is None and "Raw profile type exif" in self.info:
- exif_info = bytes.fromhex(
- "".join(self.info["Raw profile type exif"].split("\n")[3:])
- )
- self._exif.load(exif_info)
+ if exif_info is None:
+ if "Raw profile type exif" in self.info:
+ exif_info = bytes.fromhex(
+ "".join(self.info["Raw profile type exif"].split("\n")[3:])
+ )
+ elif hasattr(self, "tag_v2"):
+ self._exif.endian = self.tag_v2._endian
+ self._exif.load_from_fp(self.fp, self.tag_v2._offset)
+ if exif_info is not None:
+ self._exif.load(exif_info)
# XMP tags
if 0x0112 not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags:
- root = xml.etree.ElementTree.fromstring(xmp_tags)
- for elem in root.iter():
- if elem.tag.endswith("}Description"):
- orientation = elem.attrib.get(
- "{http://ns.adobe.com/tiff/1.0/}Orientation"
- )
- if orientation:
- self._exif[0x0112] = int(orientation)
- break
+ match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags)
+ if match:
+ self._exif[0x0112] = int(match[1])
return self._exif
@@ -1695,13 +1705,14 @@ class Image:
def putdata(self, data, scale=1.0, offset=0.0):
"""
- Copies pixel data to this image. This method copies data from a
- sequence object into the image, starting at the upper left
- corner (0, 0), and continuing until either the image or the
- sequence ends. The scale and offset values are used to adjust
- the sequence values: **pixel = value*scale + offset**.
+ Copies pixel data from a flattened sequence object into the image. The
+ values should start at the upper left corner (0, 0), continue to the
+ end of the line, followed directly by the first value of the second
+ line, and so on. Data will be read until either the image or the
+ sequence ends. The scale and offset values are used to adjust the
+ sequence values: **pixel = value*scale + offset**.
- :param data: A sequence object.
+ :param data: A flattened sequence object.
:param scale: An optional scale value. The default is 1.0.
:param offset: An optional offset value. The default is 0.0.
"""
@@ -1715,20 +1726,24 @@ class Image:
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
- The palette sequence must contain either 768 integer values, or 1024
- integer values if alpha is included. Each group of values represents
- the red, green, blue (and alpha if included) values for the
- corresponding pixel index. Instead of an integer sequence, you can use
- an 8-bit string.
+ The palette sequence must contain at most 256 colors, made up of one
+ integer value for each channel in the raw mode.
+ For example, if the raw mode is "RGB", then it can contain at most 768
+ values, made up of red, green and blue values for the corresponding pixel
+ index in the 256 colors.
+ If the raw mode is "RGBA", then it can contain at most 1024 values,
+ containing red, green, blue and alpha values.
+
+ Alternatively, an 8-bit string may be used instead of an integer sequence.
:param data: A palette sequence (either a list or a string).
- :param rawmode: The raw mode of the palette.
+ :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a
+ mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L").
"""
from . import ImagePalette
if self.mode not in ("L", "LA", "P", "PA"):
raise ValueError("illegal image mode")
- self.load()
if isinstance(data, ImagePalette.ImagePalette):
palette = ImagePalette.raw(data.rawmode, data.palette)
else:
@@ -1775,7 +1790,7 @@ class Image:
and len(value) in [3, 4]
):
# RGB or RGBA value for a P image
- value = self.palette.getcolor(value)
+ value = self.palette.getcolor(value, self)
return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None):
@@ -1796,18 +1811,17 @@ class Image:
if source_palette is None:
if self.mode == "P":
- real_source_palette = self.im.getpalette("RGB")[:768]
+ self.load()
+ source_palette = self.im.getpalette("RGB")[:768]
else: # L-mode
- real_source_palette = bytearray(i // 3 for i in range(768))
- else:
- real_source_palette = source_palette
+ source_palette = bytearray(i // 3 for i in range(768))
palette_bytes = b""
new_positions = [0] * 256
# pick only the used colors from the palette
for i, oldPosition in enumerate(dest_map):
- palette_bytes += real_source_palette[oldPosition * 3 : oldPosition * 3 + 3]
+ palette_bytes += source_palette[oldPosition * 3 : oldPosition * 3 + 3]
new_positions[oldPosition] = i
# replace the palette color id of all pixel with the new id
@@ -1833,23 +1847,19 @@ class Image:
m_im = self.copy()
m_im.mode = "P"
- m_im.palette = ImagePalette.ImagePalette(
- "RGB", palette=mapping_palette * 3, size=768
- )
+ m_im.palette = ImagePalette.ImagePalette("RGB", palette=mapping_palette * 3)
# possibly set palette dirty, then
# m_im.putpalette(mapping_palette, 'L') # converts to 'P'
# or just force it.
# UNDONE -- this is part of the general issue with palettes
- m_im.im.putpalette(*m_im.palette.getdata())
+ m_im.im.putpalette("RGB;L", m_im.palette.tobytes())
m_im = m_im.convert("L")
# Internally, we require 768 bytes for a palette.
new_palette_bytes = palette_bytes + (768 - len(palette_bytes)) * b"\x00"
m_im.putpalette(new_palette_bytes)
- m_im.palette = ImagePalette.ImagePalette(
- "RGB", palette=palette_bytes, size=len(palette_bytes)
- )
+ m_im.palette = ImagePalette.ImagePalette("RGB", palette=palette_bytes)
return m_im
@@ -1870,7 +1880,7 @@ class Image:
min(self.size[1], math.ceil(box[3] + support_y)),
)
- def resize(self, size, resample=BICUBIC, box=None, reducing_gap=None):
+ def resize(self, size, resample=None, box=None, reducing_gap=None):
"""
Returns a resized copy of this image.
@@ -1880,9 +1890,11 @@ class Image:
one of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`,
:py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`,
:py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`.
- Default filter is :py:data:`PIL.Image.BICUBIC`.
- If the image has mode "1" or "P", it is
- always set to :py:data:`PIL.Image.NEAREST`.
+ If the image has mode "1" or "P", it is always set to
+ :py:data:`PIL.Image.NEAREST`.
+ If the image mode specifies a number of bits, such as "I;16", then the
+ default filter is :py:data:`PIL.Image.NEAREST`.
+ Otherwise, the default filter is :py:data:`PIL.Image.BICUBIC`.
See: :ref:`concept-filters`.
:param box: An optional 4-tuple of floats providing
the source image region to be scaled.
@@ -1903,11 +1915,14 @@ class Image:
:returns: An :py:class:`~PIL.Image.Image` object.
"""
- if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING):
+ if resample is None:
+ type_special = ";" in self.mode
+ resample = NEAREST if type_special else BICUBIC
+ elif resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING):
message = f"Unknown resampling filter ({resample})."
filters = [
- "{} ({})".format(filter[1], filter[0])
+ f"{filter[1]} ({filter[0]})"
for filter in (
(NEAREST, "Image.NEAREST"),
(LANCZOS, "Image.LANCZOS"),
@@ -1938,7 +1953,7 @@ class Image:
resample = NEAREST
if self.mode in ["LA", "RGBA"] and resample != NEAREST:
- im = self.convert(self.mode[:-1] + "a")
+ im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
im = im.resize(size, resample, box)
return im.convert(self.mode)
@@ -1988,7 +2003,7 @@ class Image:
return self.copy()
if self.mode in ["LA", "RGBA"]:
- im = self.convert(self.mode[:-1] + "a")
+ im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
im = im.reduce(factor, box)
return im.convert(self.mode)
@@ -2039,10 +2054,8 @@ class Image:
return self.copy()
if angle == 180:
return self.transpose(ROTATE_180)
- if angle == 90 and expand:
- return self.transpose(ROTATE_90)
- if angle == 270 and expand:
- return self.transpose(ROTATE_270)
+ if angle in (90, 270) and (expand or self.width == self.height):
+ return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270)
# Calculate the affine matrix. Note that this is the reverse
# transformation (from destination image to source) because we
@@ -2145,12 +2158,17 @@ class Image:
filename = ""
open_fp = False
- if isPath(fp):
- filename = fp
- open_fp = True
- elif isinstance(fp, Path):
+ if isinstance(fp, Path):
filename = str(fp)
open_fp = True
+ elif isPath(fp):
+ filename = fp
+ open_fp = True
+ elif fp == sys.stdout:
+ try:
+ fp = sys.stdout.buffer
+ except AttributeError:
+ pass
if not filename and hasattr(fp, "name") and isPath(fp.name):
# only set the name for metadata purposes
filename = fp.name
@@ -2217,7 +2235,7 @@ class Image:
if frame != 0:
raise EOFError
- def show(self, title=None, command=None):
+ def show(self, title=None):
"""
Displays this image. This method is mainly intended for debugging purposes.
@@ -2237,14 +2255,7 @@ class Image:
:param title: Optional title to use for the image window, where possible.
"""
- if command is not None:
- warnings.warn(
- "The command parameter is deprecated and will be removed in Pillow 9 "
- "(2022-01-02). Use a subclass of ImageShow.Viewer instead.",
- DeprecationWarning,
- )
-
- _show(self, title=title, command=command)
+ _show(self, title=title)
def split(self):
"""
@@ -2421,18 +2432,11 @@ class Image:
:returns: An :py:class:`~PIL.Image.Image` object.
"""
- if self.mode == "LA" and resample != NEAREST:
+ if self.mode in ("LA", "RGBA") and resample != NEAREST:
return (
- self.convert("La")
+ self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode])
.transform(size, method, data, resample, fill, fillcolor)
- .convert("LA")
- )
-
- if self.mode == "RGBA" and resample != NEAREST:
- return (
- self.convert("RGBa")
- .transform(size, method, data, resample, fill, fillcolor)
- .convert("RGBA")
+ .convert(self.mode)
)
if isinstance(method, ImageTransformHandler):
@@ -2446,6 +2450,8 @@ class Image:
raise ValueError("missing method data")
im = new(self.mode, size, fillcolor)
+ if self.mode == "P" and self.palette:
+ im.palette = self.palette.copy()
im.info = self.info.copy()
if method == MESH:
# list of quads
@@ -2511,7 +2517,7 @@ class Image:
message = f"Unknown resampling filter ({resample})."
filters = [
- "{} ({})".format(filter[1], filter[0])
+ f"{filter[1]} ({filter[0]})"
for filter in (
(NEAREST, "Image.NEAREST"),
(BILINEAR, "Image.BILINEAR"),
@@ -2766,7 +2772,7 @@ def fromarray(obj, mode=None):
from PIL import Image
import numpy as np
- im = Image.open('hopper.jpg')
+ im = Image.open("hopper.jpg")
a = np.asarray(im)
Then this can be used to convert it to a Pillow image::
@@ -2774,8 +2780,21 @@ def fromarray(obj, mode=None):
im = Image.fromarray(a)
:param obj: Object with array interface
- :param mode: Mode to use (will be determined from type if None)
- See: :ref:`concept-modes`.
+ :param mode: Optional mode to use when reading ``obj``. Will be determined from
+ type if ``None``.
+
+ This will not be used to convert the data after reading, but will be used to
+ change how the data is read::
+
+ from PIL import Image
+ import numpy as np
+ a = np.full((1, 1), 300)
+ im = Image.fromarray(a, mode="L")
+ im.getpixel((0, 0)) # 44
+ im = Image.fromarray(a, mode="RGB")
+ im.getpixel((0, 0)) # (44, 1, 0)
+
+ See: :ref:`concept-modes` for general information about modes.
:returns: An image object.
.. versionadded:: 1.1.6
@@ -2898,7 +2917,7 @@ def open(fp, mode="r", formats=None):
:param formats: A list or tuple of formats to attempt to load the file in.
This can be used to restrict the set of formats checked.
Pass ``None`` to try all supported formats. You can print the set of
- available formats by running ``python -m PIL`` or using
+ available formats by running ``python3 -m PIL`` or using
the :py:func:`PIL.features.pilinfo` function.
:returns: An :py:class:`~PIL.Image.Image` object.
:exception FileNotFoundError: If the file cannot be found.
@@ -3208,22 +3227,9 @@ def register_encoder(name, encoder):
def _show(image, **options):
- options["_internal_pillow"] = True
- _showxv(image, **options)
-
-
-def _showxv(image, title=None, **options):
from . import ImageShow
- if "_internal_pillow" in options:
- del options["_internal_pillow"]
- else:
- warnings.warn(
- "_showxv is deprecated and will be removed in Pillow 9 (2022-01-02). "
- "Use Image.show instead.",
- DeprecationWarning,
- )
- ImageShow.show(image, title, **options)
+ ImageShow.show(image, **options)
# --------------------------------------------------------------------
@@ -3313,7 +3319,7 @@ atexit.register(core.clear_cache)
class Exif(MutableMapping):
- endian = "<"
+ endian = None
def __init__(self):
self._data = {}
@@ -3348,6 +3354,12 @@ class Exif(MutableMapping):
info.load(self.fp)
return self._fixup_dict(info)
+ def _get_head(self):
+ if self.endian == "<":
+ return b"II\x2A\x00\x08\x00\x00\x00"
+ else:
+ return b"MM\x00\x2A\x00\x00\x00\x08"
+
def load(self, data):
# Extract EXIF information. This is highly experimental,
# and is likely to be replaced with something better in a future
@@ -3360,8 +3372,8 @@ class Exif(MutableMapping):
self._loaded_exif = data
self._data.clear()
self._ifds.clear()
- self._info = None
if not data:
+ self._info = None
return
if data.startswith(b"Exif\x00\x00"):
@@ -3376,6 +3388,27 @@ class Exif(MutableMapping):
self.fp.seek(self._info.next)
self._info.load(self.fp)
+ def load_from_fp(self, fp, offset=None):
+ self._loaded_exif = None
+ self._data.clear()
+ self._ifds.clear()
+
+ # process dictionary
+ from . import TiffImagePlugin
+
+ self.fp = fp
+ if offset is not None:
+ self.head = self._get_head()
+ else:
+ self.head = self.fp.read(8)
+ self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head)
+ if self.endian is None:
+ self.endian = self._info._endian
+ if offset is None:
+ offset = self._info.next
+ self.fp.seek(offset)
+ self._info.load(self.fp)
+
def _get_merged_dict(self):
merged_dict = dict(self)
@@ -3394,10 +3427,7 @@ class Exif(MutableMapping):
def tobytes(self, offset=8):
from . import TiffImagePlugin
- if self.endian == "<":
- head = b"II\x2A\x00\x08\x00\x00\x00"
- else:
- head = b"MM\x00\x2A\x00\x00\x00\x08"
+ head = self._get_head()
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self.items():
if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict):
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 8c4740ddc..60e700f09 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -34,10 +34,10 @@ pyCMS
a Python / PIL interface to the littleCMS ICC Color Management System
Copyright (C) 2002-2003 Kevin Cazabon
kevin@cazabon.com
- http://www.cazabon.com
+ https://www.cazabon.com
- pyCMS home page: http://www.cazabon.com/pyCMS
- littleCMS home page: http://www.littlecms.com
+ pyCMS home page: https://www.cazabon.com/pyCMS
+ littleCMS home page: https://www.littlecms.com
(littleCMS is Copyright (C) 1998-2001 Marti Maria)
Originally released under LGPL. Graciously donated to PIL in
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
index 51df44040..25f92f2c7 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -32,6 +32,8 @@ def getrgb(color):
:param color: A color string
:return: ``(red, green, blue[, alpha])``
"""
+ if len(color) > 100:
+ raise ValueError("color specifier is too long")
color = color.lower()
rgb = colormap.get(color, None)
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index 8988e4233..610ccd4c7 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -33,7 +33,7 @@
import math
import numbers
-from . import Image, ImageColor
+from . import Image, ImageColor, ImageFont
"""
A simple 2D drawing interface for PIL images.
@@ -70,6 +70,7 @@ class ImageDraw:
self.palette = im.palette
else:
self.palette = None
+ self._image = im
self.im = im.im
self.draw = Image.core.draw(self.im, blend)
self.mode = mode
@@ -108,13 +109,13 @@ class ImageDraw:
if isinstance(ink, str):
ink = ImageColor.getcolor(ink, self.mode)
if self.palette and not isinstance(ink, numbers.Number):
- ink = self.palette.getcolor(ink)
+ ink = self.palette.getcolor(ink, self._image)
ink = self.draw.draw_ink(ink)
if fill is not None:
if isinstance(fill, str):
fill = ImageColor.getcolor(fill, self.mode)
if self.palette and not isinstance(fill, numbers.Number):
- fill = self.palette.getcolor(fill)
+ fill = self.palette.getcolor(fill, self._image)
fill = self.draw.draw_ink(fill)
return ink, fill
@@ -173,13 +174,11 @@ class ImageDraw:
angle -= 90
distance = width / 2 - 1
return tuple(
- [
- p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
- for p, p_d in (
- (x, distance * math.cos(math.radians(angle))),
- (y, distance * math.sin(math.radians(angle))),
- )
- ]
+ p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
+ for p, p_d in (
+ (x, distance * math.cos(math.radians(angle))),
+ (y, distance * math.sin(math.radians(angle))),
+ )
)
flipped = (
@@ -234,13 +233,35 @@ class ImageDraw:
if ink is not None:
self.draw.draw_points(xy, ink)
- def polygon(self, xy, fill=None, outline=None):
+ def polygon(self, xy, fill=None, outline=None, width=1):
"""Draw a polygon."""
ink, fill = self._getink(outline, fill)
if fill is not None:
self.draw.draw_polygon(xy, fill, 1)
- if ink is not None and ink != fill:
- self.draw.draw_polygon(xy, ink, 0)
+ if ink is not None and ink != fill and width != 0:
+ if width == 1:
+ self.draw.draw_polygon(xy, ink, 0, width)
+ else:
+ # To avoid expanding the polygon outwards,
+ # use the fill as a mask
+ mask = Image.new("1", self.im.size)
+ mask_ink = self._getink(1)[0]
+
+ fill_im = mask.copy()
+ draw = Draw(fill_im)
+ draw.draw.draw_polygon(xy, mask_ink, 1)
+
+ ink_im = mask.copy()
+ draw = Draw(ink_im)
+ width = width * 2 - 1
+ draw.draw.draw_polygon(xy, mask_ink, 0, width)
+
+ mask.paste(ink_im, mask=fill_im)
+
+ im = Image.new(self.mode, self.im.size)
+ draw = Draw(im)
+ draw.draw.draw_polygon(xy, ink, 0, width)
+ self.im.paste(im.im, (0, 0) + im.size, mask.im)
def regular_polygon(
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None
@@ -282,6 +303,7 @@ class ImageDraw:
# If the corners have no curve, that is a rectangle
return self.rectangle(xy, fill, outline, width)
+ r = d // 2
ink, fill = self._getink(outline, fill)
def draw_corners(pieslice):
@@ -315,36 +337,28 @@ class ImageDraw:
draw_corners(True)
if full_x:
- self.draw.draw_rectangle(
- (x0, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1
- )
+ self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1)
else:
- self.draw.draw_rectangle(
- (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1
- )
+ self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1)
if not full_x and not full_y:
- self.draw.draw_rectangle(
- (x0, y0 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1
- )
- self.draw.draw_rectangle(
- (x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1
- )
+ self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1)
+ self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1)
if ink is not None and ink != fill and width != 0:
draw_corners(False)
if not full_x:
self.draw.draw_rectangle(
- (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1
+ (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1
)
self.draw.draw_rectangle(
- (x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1
+ (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1
)
if not full_y:
self.draw.draw_rectangle(
- (x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1
+ (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1
)
self.draw.draw_rectangle(
- (x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1
+ (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1
)
def _multiline_check(self, text):
@@ -653,6 +667,8 @@ class ImageDraw:
if font is None:
font = self.getfont()
+ if not isinstance(font, ImageFont.FreeTypeFont):
+ raise ValueError("Only supported for TrueType fonts")
mode = "RGBA" if embedded_color else self.fontmode
bbox = font.getbbox(
text, mode, direction, features, language, stroke_width, anchor
@@ -983,6 +999,6 @@ def _color_diff(color1, color2):
Uses 1-norm distance to calculate difference between two values.
"""
if isinstance(color2, tuple):
- return sum([abs(color1[i] - color2[i]) for i in range(0, len(color2))])
+ return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2)))
else:
return abs(color1 - color2)
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index f58de95bd..3374a5b1d 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -28,9 +28,9 @@
#
import io
+import itertools
import struct
import sys
-import warnings
from . import Image
from ._util import isPath
@@ -67,15 +67,6 @@ def raise_oserror(error):
raise OSError(message + " when reading image file")
-def raise_ioerror(error):
- warnings.warn(
- "raise_ioerror is deprecated and will be removed in Pillow 9 (2022-01-02). "
- "Use raise_oserror instead.",
- DeprecationWarning,
- )
- return raise_oserror(error)
-
-
def _tilesort(t):
# sort on offset
return t[2]
@@ -220,6 +211,13 @@ class ImageFile(Image.Image):
except AttributeError:
prefix = b""
+ # Remove consecutive duplicates that only differ by their offset
+ self.tile = [
+ list(tiles)[-1]
+ for _, tiles in itertools.groupby(
+ self.tile, lambda tile: (tile[0], tile[1], tile[3])
+ )
+ ]
for decoder_name, extents, offset, args in self.tile:
decoder = Image._getdecoder(
self.mode, decoder_name, args, self.decoderconfig
@@ -493,9 +491,6 @@ def _save(im, fp, tile, bufsize=0):
# But, it would need at least the image size in most cases. RawEncode is
# a tricky case.
bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
- if fp == sys.stdout:
- fp.flush()
- return
try:
fh = fp.fileno()
fp.flush()
@@ -545,19 +540,28 @@ def _safe_read(fp, size):
:param fp: File handle. Must implement a read method.
:param size: Number of bytes to read.
- :returns: A string containing up to size bytes of data.
+ :returns: A string containing size bytes of data.
+
+ Raises an OSError if the file is truncated and the read cannot be completed
+
"""
if size <= 0:
return b""
if size <= SAFEBLOCK:
- return fp.read(size)
+ data = fp.read(size)
+ if len(data) < size:
+ raise OSError("Truncated File Read")
+ return data
data = []
- while size > 0:
- block = fp.read(min(size, SAFEBLOCK))
+ remaining_size = size
+ while remaining_size > 0:
+ block = fp.read(min(remaining_size, SAFEBLOCK))
if not block:
break
data.append(block)
- size -= len(block)
+ remaining_size -= len(block)
+ if sum(len(d) for d in data) < size:
+ raise OSError("Truncated File Read")
return b"".join(data)
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index 6800bc3a0..d2ece3752 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -149,9 +149,11 @@ class ModeFilter(Filter):
class GaussianBlur(MultibandFilter):
- """Gaussian blur filter.
+ """Blurs the image with a sequence of extended box filters, which
+ approximates a Gaussian kernel. For details on accuracy see
+
- :param radius: Blur radius.
+ :param radius: Standard deviation of the Gaussian kernel.
"""
name = "GaussianBlur"
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index c48d89835..805c8fff9 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -28,10 +28,9 @@
import base64
import os
import sys
-import warnings
from io import BytesIO
-from . import Image, features
+from . import Image
from ._util import isDirectory, isPath
LAYOUT_BASIC = 0
@@ -165,21 +164,6 @@ class FreeTypeFont:
self.index = index
self.encoding = encoding
- try:
- from packaging.version import parse as parse_version
- except ImportError:
- pass
- else:
- freetype_version = parse_version(features.version_module("freetype2"))
- if freetype_version < parse_version("2.8"):
- warnings.warn(
- "Support for FreeType 2.7 is deprecated and will be removed"
- " in Pillow 9 (2022-01-02). Please upgrade to FreeType 2.8 "
- "or newer, preferably FreeType 2.10.4 which fixes "
- "CVE-2020-15999.",
- DeprecationWarning,
- )
-
if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM):
layout_engine = LAYOUT_BASIC
if core.HAVE_RAQM:
@@ -212,6 +196,13 @@ class FreeTypeFont:
else:
load_from_bytes(font)
+ def __getstate__(self):
+ return [self.path, self.size, self.index, self.encoding, self.layout_engine]
+
+ def __setstate__(self, state):
+ path, size, index, encoding, layout_engine = state
+ self.__init__(path, size, index, encoding, layout_engine)
+
def _multiline_split(self, text):
split_character = "\n" if isinstance(text, str) else b"\n"
return text.split(split_character)
@@ -669,6 +660,7 @@ class FreeTypeFont:
)
size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
offset = offset[0] - stroke_width, offset[1] - stroke_width
+ Image._decompression_bomb_check(size)
im = fill("RGBA" if mode == "RGBA" else "L", size, 0)
self.font.render(
text, im.id, mode, direction, features, language, stroke_width, ink
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index 7f9c88e14..06bea800d 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -246,7 +246,12 @@ def eval(expression, _dict={}, **kw):
if hasattr(v, "im"):
args[k] = _Operand(v)
- out = builtins.eval(expression, args)
+ code = compile(expression, "", "eval")
+ for name in code.co_names:
+ if name not in args and name != "abs":
+ raise ValueError(f"'{name}' not allowed")
+
+ out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args)
try:
return out.im
except AttributeError:
diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py
index 988288329..0afcf9fe1 100644
--- a/src/PIL/ImageMode.py
+++ b/src/PIL/ImageMode.py
@@ -35,18 +35,28 @@ def getmode(mode):
global _modes
if not _modes:
# initialize mode cache
-
- from . import Image
-
modes = {}
- # core modes
- for m, (basemode, basetype, bands) in Image._MODEINFO.items():
+ for m, (basemode, basetype, bands) in {
+ # core modes
+ "1": ("L", "L", ("1",)),
+ "L": ("L", "L", ("L",)),
+ "I": ("L", "I", ("I",)),
+ "F": ("L", "F", ("F",)),
+ "P": ("P", "L", ("P",)),
+ "RGB": ("RGB", "L", ("R", "G", "B")),
+ "RGBX": ("RGB", "L", ("R", "G", "B", "X")),
+ "RGBA": ("RGB", "L", ("R", "G", "B", "A")),
+ "CMYK": ("RGB", "L", ("C", "M", "Y", "K")),
+ "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")),
+ "LAB": ("RGB", "L", ("L", "A", "B")),
+ "HSV": ("RGB", "L", ("H", "S", "V")),
+ # extra experimental modes
+ "RGBa": ("RGB", "L", ("R", "G", "B", "a")),
+ "LA": ("L", "L", ("L", "A")),
+ "La": ("L", "L", ("L", "a")),
+ "PA": ("RGB", "L", ("P", "A")),
+ }.items():
modes[m] = ModeDescriptor(m, bands, basemode, basetype)
- # extra experimental modes
- modes["RGBa"] = ModeDescriptor("RGBa", ("R", "G", "B", "a"), "RGB", "L")
- modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L")
- modes["La"] = ModeDescriptor("La", ("L", "a"), "L", "L")
- modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L")
# mapping modes
for i16mode in (
"I;16",
diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py
index b76dfa01f..fe0083754 100644
--- a/src/PIL/ImageMorph.py
+++ b/src/PIL/ImageMorph.py
@@ -196,7 +196,7 @@ class MorphOp:
raise Exception("No operator loaded")
if image.mode != "L":
- raise Exception("Image must be binary, meaning it must use mode L")
+ raise ValueError("Image mode must be L")
outimage = Image.new(image.mode, image.size, None)
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
@@ -211,7 +211,7 @@ class MorphOp:
raise Exception("No operator loaded")
if image.mode != "L":
- raise Exception("Image must be binary, meaning it must use mode L")
+ raise ValueError("Image mode must be L")
return _imagingmorph.match(bytes(self.lut), image.im.id)
def get_on_pixels(self, image):
@@ -221,7 +221,7 @@ class MorphOp:
of all matching pixels. See :ref:`coordinate-system`."""
if image.mode != "L":
- raise Exception("Image must be binary, meaning it must use mode L")
+ raise ValueError("Image mode must be L")
return _imagingmorph.get_on_pixels(image.im.id)
def load_lut(self, filename):
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 14602a5c8..b170e9d8c 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -19,6 +19,7 @@
import functools
import operator
+import re
from . import Image
@@ -61,7 +62,7 @@ def _lut(image, lut):
# actions
-def autocontrast(image, cutoff=0, ignore=None, mask=None):
+def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
"""
Maximize (normalize) image contrast. This function calculates a
histogram of the input image (or mask region), removes ``cutoff`` percent of the
@@ -77,9 +78,17 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None):
:param mask: Histogram used in contrast operation is computed using pixels
within the mask. If no mask is given the entire image is used
for histogram computation.
+ :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
+
+ .. versionadded:: 8.2.0
+
:return: An image.
"""
- histogram = image.histogram(mask)
+ if preserve_tone:
+ histogram = image.convert("L").histogram(mask)
+ else:
+ histogram = image.histogram(mask)
+
lut = []
for layer in range(0, len(histogram), 256):
h = histogram[layer : layer + 256]
@@ -228,15 +237,43 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
return _lut(image, red + green + blue)
-def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)):
+def contain(image, size, method=Image.BICUBIC):
"""
- Returns a sized and padded version of the image, expanded to fill the
- requested aspect ratio and size.
+ Returns a resized version of the image, set to the maximum width and height
+ within the requested size, while maintaining the original aspect ratio.
- :param image: The image to size and crop.
+ :param image: The image to resize and crop.
:param size: The requested output size in pixels, given as a
(width, height) tuple.
- :param method: What resampling method to use. Default is
+ :param method: Resampling method to use. Default is
+ :py:attr:`PIL.Image.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 = int(image.height / image.width * size[0])
+ if new_height != size[1]:
+ size = (size[0], new_height)
+ else:
+ new_width = int(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.BICUBIC, color=None, centering=(0.5, 0.5)):
+ """
+ Returns a resized and padded version of the image, expanded to fill the
+ requested aspect ratio and size.
+
+ :param image: The image to resize and crop.
+ :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.BICUBIC`. See :ref:`concept-filters`.
:param color: The background color of the padded image.
:param centering: Control the position of the original image within the
@@ -249,27 +286,17 @@ def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)):
:return: An image.
"""
- im_ratio = image.width / image.height
- dest_ratio = size[0] / size[1]
-
- if im_ratio == dest_ratio:
- out = image.resize(size, resample=method)
+ resized = contain(image, size, method)
+ if resized.size == size:
+ out = resized
else:
out = Image.new(image.mode, size, color)
- if im_ratio > dest_ratio:
- new_height = int(image.height / image.width * size[0])
- if new_height != size[1]:
- image = image.resize((size[0], new_height), resample=method)
-
- y = int((size[1] - new_height) * max(0, min(centering[1], 1)))
- out.paste(image, (0, y))
+ if resized.width != size[0]:
+ x = int((size[0] - resized.width) * max(0, min(centering[0], 1)))
+ out.paste(resized, (x, 0))
else:
- new_width = int(image.width / image.height * size[1])
- if new_width != size[0]:
- image = image.resize((new_width, size[1]), resample=method)
-
- x = int((size[0] - new_width) * max(0, min(centering[0], 1)))
- out.paste(image, (x, 0))
+ y = int((size[1] - resized.height) * max(0, min(centering[1], 1)))
+ out.paste(resized, (0, y))
return out
@@ -296,7 +323,7 @@ def scale(image, factor, resample=Image.BICUBIC):
:param image: The image to rescale.
:param factor: The expansion factor, as a float.
- :param resample: What resampling method to use. Default is
+ :param resample: Resampling method to use. Default is
:py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
:returns: An :py:class:`~PIL.Image.Image` object.
"""
@@ -366,22 +393,32 @@ def expand(image, border=0, fill=0):
left, top, right, bottom = _border(border)
width = left + image.size[0] + right
height = top + image.size[1] + bottom
- out = Image.new(image.mode, (width, height), _color(fill, image.mode))
+ color = _color(fill, image.mode)
+ if image.mode == "P" and image.palette:
+ image.load()
+ palette = image.palette.copy()
+ if isinstance(color, tuple):
+ color = palette.getcolor(color)
+ else:
+ palette = None
+ out = Image.new(image.mode, (width, height), color)
+ if palette:
+ out.putpalette(palette.palette)
out.paste(image, (left, top))
return out
def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
"""
- Returns a sized and cropped version of the image, cropped to the
+ Returns a resized and cropped version of the image, cropped to the
requested aspect ratio and size.
This function was contributed by Kevin Cazabon.
- :param image: The image to size and crop.
+ :param image: The image to resize and crop.
:param size: The requested output size in pixels, given as a
(width, height) tuple.
- :param method: What resampling method to use. Default is
+ :param method: Resampling method to use. Default is
:py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
:param bleed: Remove a border around the outside of the image from all
four edges. The value is a decimal percentage (use 0.01 for
@@ -402,7 +439,7 @@ def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
# by Kevin Cazabon, Feb 17/2000
# kevin@cazabon.com
- # http://www.cazabon.com
+ # https://www.cazabon.com
# ensure centering is mutable
centering = list(centering)
@@ -552,7 +589,20 @@ def exif_transpose(image):
}.get(orientation)
if method is not None:
transposed_image = image.transpose(method)
- del exif[0x0112]
- transposed_image.info["exif"] = exif.tobytes()
+ transposed_exif = transposed_image.getexif()
+ if 0x0112 in transposed_exif:
+ del transposed_exif[0x0112]
+ if "exif" in transposed_image.info:
+ transposed_image.info["exif"] = transposed_exif.tobytes()
+ elif "Raw profile type exif" in transposed_image.info:
+ transposed_image.info[
+ "Raw profile type exif"
+ ] = transposed_exif.tobytes().hex()
+ elif "XML:com.adobe.xmp" in transposed_image.info:
+ transposed_image.info["XML:com.adobe.xmp"] = re.sub(
+ r'tiff:Orientation="([0-9])"',
+ "",
+ transposed_image.info["XML:com.adobe.xmp"],
+ )
return transposed_image
return image.copy()
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index d0604112f..1e0d36b41 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -17,6 +17,7 @@
#
import array
+import warnings
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@@ -25,27 +26,45 @@ class ImagePalette:
"""
Color palette for palette mapped images
- :param mode: The mode to use for the Palette. See:
+ :param mode: The mode to use for the palette. See:
:ref:`concept-modes`. Defaults to "RGB"
:param palette: An optional palette. If given, it must be a bytearray,
- an array or a list of ints between 0-255 and of length ``size``
- times the number of colors in ``mode``. The list must be aligned
- by channel (All R values must be contiguous in the list before G
- and B values.) Defaults to 0 through 255 per channel.
- :param size: An optional palette size. If given, it cannot be equal to
- or greater than 256. Defaults to 0.
+ an array or a list of ints between 0-255. The list must consist of
+ all channels for one color followed by the next color (e.g. RGBRGBRGB).
+ Defaults to an empty palette.
+ :param size: An optional palette size. If given, an error is raised
+ if ``palette`` is not of equal length.
"""
def __init__(self, mode="RGB", palette=None, size=0):
self.mode = mode
self.rawmode = None # if set, palette contains raw data
- self.palette = palette or bytearray(range(256)) * len(self.mode)
- self.colors = {}
+ self.palette = palette or bytearray()
self.dirty = None
- if (size == 0 and len(self.mode) * 256 != len(self.palette)) or (
- size != 0 and size != len(self.palette)
- ):
- raise ValueError("wrong palette size")
+ if size != 0:
+ warnings.warn(
+ "The size parameter is deprecated and will be removed in Pillow 10 "
+ "(2023-07-01).",
+ DeprecationWarning,
+ )
+ if size != len(self.palette):
+ raise ValueError("wrong palette size")
+
+ @property
+ def palette(self):
+ return self._palette
+
+ @palette.setter
+ def palette(self, palette):
+ self._palette = palette
+
+ mode_len = len(self.mode)
+ self.colors = {}
+ for i in range(0, len(self.palette), mode_len):
+ color = tuple(self.palette[i : i + mode_len])
+ if color in self.colors:
+ continue
+ self.colors[color] = i // mode_len
def copy(self):
new = ImagePalette()
@@ -54,7 +73,6 @@ class ImagePalette:
new.rawmode = self.rawmode
if self.palette is not None:
new.palette = self.palette[:]
- new.colors = self.colors.copy()
new.dirty = self.dirty
return new
@@ -68,7 +86,7 @@ class ImagePalette:
"""
if self.rawmode:
return self.rawmode, self.palette
- return self.mode + ";L", self.tobytes()
+ return self.mode, self.tobytes()
def tobytes(self):
"""Convert palette to bytes.
@@ -80,14 +98,12 @@ class ImagePalette:
if isinstance(self.palette, bytes):
return self.palette
arr = array.array("B", self.palette)
- if hasattr(arr, "tobytes"):
- return arr.tobytes()
- return arr.tostring()
+ return arr.tobytes()
# Declare tostring as an alias for tobytes
tostring = tobytes
- def getcolor(self, color):
+ def getcolor(self, color, image=None):
"""Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental.
@@ -95,19 +111,45 @@ class ImagePalette:
if self.rawmode:
raise ValueError("palette contains raw palette data")
if isinstance(color, tuple):
+ if self.mode == "RGB":
+ if len(color) == 4 and color[3] == 255:
+ color = color[:3]
+ elif self.mode == "RGBA":
+ if len(color) == 3:
+ color += (255,)
try:
return self.colors[color]
except KeyError as e:
# allocate new color slot
- if isinstance(self.palette, bytes):
- self.palette = bytearray(self.palette)
- index = len(self.colors)
+ if not isinstance(self.palette, bytearray):
+ self._palette = bytearray(self.palette)
+ index = len(self.palette) // 3
+ special_colors = ()
+ if image:
+ special_colors = (
+ image.info.get("background"),
+ image.info.get("transparency"),
+ )
+ while index in special_colors:
+ index += 1
if index >= 256:
- raise ValueError("cannot allocate more than 256 colors") from e
+ if image:
+ # Search for an unused index
+ for i, count in reversed(list(enumerate(image.histogram()))):
+ if count == 0 and i not in special_colors:
+ index = i
+ break
+ if index >= 256:
+ raise ValueError("cannot allocate more than 256 colors") from e
self.colors[color] = index
- self.palette[index] = color[0]
- self.palette[index + 256] = color[1]
- self.palette[index + 512] = color[2]
+ if index * 3 < len(self.palette):
+ self._palette = (
+ self.palette[: index * 3]
+ + bytes(color)
+ + self.palette[index * 3 + 3 :]
+ )
+ else:
+ self._palette += bytes(color)
self.dirty = 1
return index
else:
@@ -169,9 +211,9 @@ def make_gamma_lut(exp):
def negative(mode="RGB"):
- palette = list(range(256))
+ palette = list(range(256 * len(mode)))
palette.reverse()
- return ImagePalette(mode, palette * len(mode))
+ return ImagePalette(mode, [i // len(mode) for i in palette])
def random(mode="RGB"):
@@ -184,15 +226,13 @@ def random(mode="RGB"):
def sepia(white="#fff0c0"):
- r, g, b = ImageColor.getrgb(white)
- r = make_linear_lut(0, r)
- g = make_linear_lut(0, g)
- b = make_linear_lut(0, b)
- return ImagePalette("RGB", r + g + b)
+ bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
+ return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
def wedge(mode="RGB"):
- return ImagePalette(mode, list(range(256)) * len(mode))
+ palette = list(range(256 * len(mode)))
+ return ImagePalette(mode, [i // len(mode) for i in palette])
def load(filename):
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index 32630f2ca..db8fa0fa9 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -66,7 +66,13 @@ def fromqimage(im):
:param im: QImage or PIL ImageQt object
"""
buffer = QBuffer()
- qt_openmode = QIODevice.OpenMode if qt_version == "6" else QIODevice
+ if qt_version == "6":
+ try:
+ qt_openmode = QIODevice.OpenModeFlag
+ except AttributeError:
+ qt_openmode = QIODevice.OpenMode
+ else:
+ qt_openmode = QIODevice
buffer.open(qt_openmode.ReadWrite)
# preserve alpha channel with png
# otherwise ppm is more friendly with Image.open
@@ -102,7 +108,7 @@ def align8to32(bytes, width, mode):
converts each scanline of data from 8 bit to 32 bit aligned
"""
- bits_per_pixel = {"1": 1, "L": 8, "P": 8}[mode]
+ bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode]
# calculate bytes per line and the extra padding if needed
bits_per_line = bits_per_pixel * width
@@ -161,6 +167,10 @@ def _toqclass_helper(im):
elif im.mode == "RGBA":
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_ARGB32
+ elif im.mode == "I;16" and hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+
+ im = im.point(lambda i: i * 256)
+
+ format = qt_format.Format_Grayscale16
else:
if exclusive_fp:
im.close()
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 3368865a4..2135293e5 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -133,7 +133,7 @@ if sys.platform == "win32":
class MacViewer(Viewer):
- """The default viewer on MacOS using ``Preview.app``."""
+ """The default viewer on macOS using ``Preview.app``."""
format = "PNG"
options = {"compress_level": 1}
@@ -186,11 +186,35 @@ class UnixViewer(Viewer):
return 1
-class DisplayViewer(UnixViewer):
- """The ImageMagick ``display`` command."""
+class XDGViewer(UnixViewer):
+ """
+ The freedesktop.org ``xdg-open`` command.
+ """
def get_command_ex(self, file, **options):
+ command = executable = "xdg-open"
+ return command, executable
+
+
+class DisplayViewer(UnixViewer):
+ """
+ The ImageMagick ``display`` command.
+ This viewer supports the ``title`` parameter.
+ """
+
+ def get_command_ex(self, file, title=None, **options):
command = executable = "display"
+ if title:
+ command += f" -name {quote(title)}"
+ return command, executable
+
+
+class GmDisplayViewer(UnixViewer):
+ """The GraphicsMagick ``gm display`` command."""
+
+ def get_command_ex(self, file, **options):
+ executable = "gm"
+ command = "gm display"
return command, executable
@@ -198,7 +222,8 @@ class EogViewer(UnixViewer):
"""The GNOME Image Viewer ``eog`` command."""
def get_command_ex(self, file, **options):
- command = executable = "eog"
+ executable = "eog"
+ command = "eog -n"
return command, executable
@@ -218,8 +243,12 @@ class XVViewer(UnixViewer):
if sys.platform not in ("win32", "darwin"): # unixoids
+ if shutil.which("xdg-open"):
+ register(XDGViewer)
if shutil.which("display"):
register(DisplayViewer)
+ if shutil.which("gm"):
+ register(GmDisplayViewer)
if shutil.which("eog"):
register(EogViewer)
if shutil.which("xv"):
@@ -245,7 +274,7 @@ else:
if __name__ == "__main__":
if len(sys.argv) < 2:
- print("Syntax: python ImageShow.py imagefile [title]")
+ print("Syntax: python3 ImageShow.py imagefile [title]")
sys.exit()
with Image.open(sys.argv[1]) as im:
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 0b0d433db..cc7980278 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -6,6 +6,7 @@
#
# History:
# 2014-03-12 ajh Created
+# 2021-06-30 rogermb Extract dpi information from the 'resc' header box
#
# Copyright (c) 2014 Coriolis Systems Limited
# Copyright (c) 2014 Alastair Houghton
@@ -19,6 +20,79 @@ import struct
from . import Image, ImageFile
+class BoxReader:
+ """
+ A small helper class to read fields stored in JPEG2000 header boxes
+ and to easily step into and read sub-boxes.
+ """
+
+ def __init__(self, fp, length=-1):
+ self.fp = fp
+ self.has_length = length >= 0
+ self.length = length
+ self.remaining_in_box = -1
+
+ def _can_read(self, num_bytes):
+ if self.has_length and self.fp.tell() + num_bytes > self.length:
+ # Outside box: ensure we don't read past the known file length
+ return False
+ if self.remaining_in_box >= 0:
+ # Inside box contents: ensure read does not go past box boundaries
+ return num_bytes <= self.remaining_in_box
+ else:
+ return True # No length known, just read
+
+ def _read_bytes(self, num_bytes):
+ if not self._can_read(num_bytes):
+ raise SyntaxError("Not enough data in header")
+
+ data = self.fp.read(num_bytes)
+ if len(data) < num_bytes:
+ raise OSError(
+ f"Expected to read {num_bytes} bytes but only got {len(data)}."
+ )
+
+ if self.remaining_in_box > 0:
+ self.remaining_in_box -= num_bytes
+ return data
+
+ def read_fields(self, field_format):
+ size = struct.calcsize(field_format)
+ data = self._read_bytes(size)
+ return struct.unpack(field_format, data)
+
+ def read_boxes(self):
+ size = self.remaining_in_box
+ data = self._read_bytes(size)
+ return BoxReader(io.BytesIO(data), size)
+
+ def has_next_box(self):
+ if self.has_length:
+ return self.fp.tell() + self.remaining_in_box < self.length
+ else:
+ return True
+
+ def next_box_type(self):
+ # Skip the rest of the box if it has not been read
+ if self.remaining_in_box > 0:
+ self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
+ self.remaining_in_box = -1
+
+ # Read the length and type of the next box
+ lbox, tbox = self.read_fields(">I4s")
+ if lbox == 1:
+ lbox = self.read_fields(">Q")[0]
+ hlen = 16
+ else:
+ hlen = 8
+
+ if lbox < hlen or not self._can_read(lbox - hlen):
+ raise SyntaxError("Invalid header length")
+
+ self.remaining_in_box = lbox - hlen
+ return tbox
+
+
def _parse_codestream(fp):
"""Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@@ -53,101 +127,71 @@ def _parse_codestream(fp):
return (size, mode)
+def _res_to_dpi(num, denom, exp):
+ """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
+ calculated as (num / denom) * 10^exp and stored in dots per meter,
+ to floating-point dots per inch."""
+ if denom != 0:
+ return (254 * num * (10 ** exp)) / (10000 * denom)
+
+
def _parse_jp2_header(fp):
- """Parse the JP2 header box to extract size, component count and
- color space information, returning a (size, mode, mimetype) tuple."""
+ """Parse the JP2 header box to extract size, component count,
+ color space information, and optionally DPI information,
+ returning a (size, mode, mimetype, dpi) tuple."""
# Find the JP2 header box
+ reader = BoxReader(fp)
header = None
mimetype = None
- while True:
- lbox, tbox = struct.unpack(">I4s", fp.read(8))
- if lbox == 1:
- lbox = struct.unpack(">Q", fp.read(8))[0]
- hlen = 16
- else:
- hlen = 8
-
- if lbox < hlen:
- raise SyntaxError("Invalid JP2 header length")
+ while reader.has_next_box():
+ tbox = reader.next_box_type()
if tbox == b"jp2h":
- header = fp.read(lbox - hlen)
+ header = reader.read_boxes()
break
elif tbox == b"ftyp":
- if fp.read(4) == b"jpx ":
+ if reader.read_fields(">4s")[0] == b"jpx ":
mimetype = "image/jpx"
- fp.seek(lbox - hlen - 4, os.SEEK_CUR)
- else:
- fp.seek(lbox - hlen, os.SEEK_CUR)
-
- if header is None:
- raise SyntaxError("could not find JP2 header")
size = None
mode = None
bpc = None
nc = None
+ dpi = None # 2-tuple of DPI info, or None
- hio = io.BytesIO(header)
- while True:
- lbox, tbox = struct.unpack(">I4s", hio.read(8))
- if lbox == 1:
- lbox = struct.unpack(">Q", hio.read(8))[0]
- hlen = 16
- else:
- hlen = 8
-
- content = hio.read(lbox - hlen)
+ while header.has_next_box():
+ tbox = header.next_box_type()
if tbox == b"ihdr":
- height, width, nc, bpc, c, unkc, ipr = struct.unpack(">IIHBBBB", content)
+ height, width, nc, bpc = header.read_fields(">IIHB")
size = (width, height)
- if unkc:
- if nc == 1 and (bpc & 0x7F) > 8:
- mode = "I;16"
- elif nc == 1:
- mode = "L"
- elif nc == 2:
- mode = "LA"
- elif nc == 3:
- mode = "RGB"
- elif nc == 4:
- mode = "RGBA"
- break
- elif tbox == b"colr":
- meth, prec, approx = struct.unpack_from(">BBB", content)
- if meth == 1:
- cs = struct.unpack_from(">I", content, 3)[0]
- if cs == 16: # sRGB
- if nc == 1 and (bpc & 0x7F) > 8:
- mode = "I;16"
- elif nc == 1:
- mode = "L"
- elif nc == 3:
- mode = "RGB"
- elif nc == 4:
- mode = "RGBA"
- break
- elif cs == 17: # grayscale
- if nc == 1 and (bpc & 0x7F) > 8:
- mode = "I;16"
- elif nc == 1:
- mode = "L"
- elif nc == 2:
- mode = "LA"
- break
- elif cs == 18: # sYCC
- if nc == 3:
- mode = "RGB"
- elif nc == 4:
- mode = "RGBA"
+ if nc == 1 and (bpc & 0x7F) > 8:
+ mode = "I;16"
+ elif nc == 1:
+ mode = "L"
+ elif nc == 2:
+ mode = "LA"
+ elif nc == 3:
+ mode = "RGB"
+ elif nc == 4:
+ mode = "RGBA"
+ elif tbox == b"res ":
+ res = header.read_boxes()
+ while res.has_next_box():
+ tres = res.next_box_type()
+ if tres == b"resc":
+ vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
+ hres = _res_to_dpi(hrcn, hrcd, hrce)
+ vres = _res_to_dpi(vrcn, vrcd, vrce)
+ if hres is not None and vres is not None:
+ dpi = (hres, vres)
break
if size is None or mode is None:
- raise SyntaxError("Malformed jp2 header")
+ raise SyntaxError("Malformed JP2 header")
- return (size, mode, mimetype)
+ return (size, mode, mimetype, dpi)
##
@@ -169,7 +213,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
self.codec = "jp2"
header = _parse_jp2_header(self.fp)
- self._size, self.mode, self.custom_mimetype = header
+ self._size, self.mode, self.custom_mimetype, dpi = header
+ if dpi is not None:
+ self.info["dpi"] = dpi
else:
raise SyntaxError("not a JPEG 2000 file")
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index ad260acbd..ccdcc20a8 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -33,6 +33,7 @@
#
import array
import io
+import math
import os
import struct
import subprocess
@@ -139,8 +140,8 @@ def APP(self, marker):
self.info["adobe"] = i16(s, 5)
# extract Adobe custom properties
try:
- adobe_transform = s[1]
- except Exception:
+ adobe_transform = s[11]
+ except IndexError:
pass
else:
self.info["adobe_transform"] = adobe_transform
@@ -161,15 +162,17 @@ def APP(self, marker):
dpi = float(x_resolution[0]) / x_resolution[1]
except TypeError:
dpi = x_resolution
+ if math.isnan(dpi):
+ raise ValueError
if resolution_unit == 3: # cm
# 1 dpcm = 2.54 dpi
dpi *= 2.54
- self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5)
- except (KeyError, SyntaxError, ValueError, ZeroDivisionError):
+ self.info["dpi"] = dpi, dpi
+ except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
# SyntaxError for invalid/unreadable EXIF
# KeyError for dpi not included
# ZeroDivisionError for invalid dpi rational value
- # ValueError for x_resolution[0] being an invalid float
+ # ValueError or TypeError for dpi being an invalid float
self.info["dpi"] = 72, 72
@@ -251,7 +254,7 @@ def DQT(self, marker):
data = array.array("B" if precision == 1 else "H", s[1:qt_length])
if sys.byteorder == "little" and precision > 1:
data.byteswap() # the values are always big-endian
- self.quantization[v & 15] = data
+ self.quantization[v & 15] = [data[i] for i in zigzag_index]
s = s[qt_length:]
@@ -398,9 +401,10 @@ class JpegImageFile(ImageFile.ImageFile):
"""
s = self.fp.read(read_bytes)
- if not s and ImageFile.LOAD_TRUNCATED_IMAGES:
+ if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"):
# Premature EOF.
# Pretend file is finished adding EOI marker
+ self._ended = True
return b"\xFF\xD9"
return s
@@ -474,6 +478,20 @@ class JpegImageFile(ImageFile.ImageFile):
def _getmp(self):
return _getmp(self)
+ def getxmp(self):
+ """
+ Returns a dictionary containing the XMP tags.
+ Requires defusedxml to be installed.
+ :returns: XMP tags in a dictionary.
+ """
+
+ for segment, content in self.applist:
+ if segment == "APP1":
+ marker, xmp_tags = content.rsplit(b"\x00", 1)
+ if marker == b"http://ns.adobe.com/xap/1.0/":
+ return self._getxmp(xmp_tags)
+ return {}
+
def _getexif(self):
if "exif" not in self.info:
@@ -584,9 +602,11 @@ samplings = {
def convert_dict_qtables(qtables):
- qtables = [qtables[key] for key in range(len(qtables)) if key in qtables]
- for idx, table in enumerate(qtables):
- qtables[idx] = [table[i] for i in zigzag_index]
+ warnings.warn(
+ "convert_dict_qtables is deprecated and will be removed in Pillow 10"
+ "(2023-07-01). Conversion is no longer needed.",
+ DeprecationWarning,
+ )
return qtables
@@ -667,7 +687,9 @@ def _save(im, fp, filename):
qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)]
if isinstance(qtables, (tuple, list, dict)):
if isinstance(qtables, dict):
- qtables = convert_dict_qtables(qtables)
+ qtables = [
+ qtables[key] for key in range(len(qtables)) if key in qtables
+ ]
elif isinstance(qtables, tuple):
qtables = list(qtables)
if not (0 < len(qtables) < 5):
diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py
index 79d10ebb2..e5a5d178a 100644
--- a/src/PIL/JpegPresets.py
+++ b/src/PIL/JpegPresets.py
@@ -52,19 +52,11 @@ You can get the quantization tables of a JPEG with::
im.quantization
-This will return a dict with a number of arrays. You can pass this dict
+This will return a dict with a number of lists. You can pass this dict
directly as the qtables argument when saving a JPEG.
-The tables format between im.quantization and quantization in presets differ in
-3 ways:
-
-1. The base container of the preset is a list with sublists instead of dict.
- dict[0] -> list[0], dict[1] -> list[1], ...
-2. Each table in a preset is a list instead of an array.
-3. The zigzag order is remove in the preset (needed by libjpeg >= 6a).
-
-You can convert the dict format to the preset format with the
-:func:`.JpegImagePlugin.convert_dict_qtables()` function.
+The quantization table format in presets is a list with sublists. These formats
+are interchangeable.
Libjpeg ref.:
https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 7244aa2a9..7ccf27c42 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -21,9 +21,8 @@
from . import Image, ImageFile, JpegImagePlugin
from ._binary import i16be as i16
-
-def _accept(prefix):
- return JpegImagePlugin._accept(prefix)
+# def _accept(prefix):
+# return JpegImagePlugin._accept(prefix)
def _save(im, fp, filename):
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index e1fdc1fdf..32b28d44d 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -21,7 +21,7 @@
# Figure 205. Windows Paint Version 1: "DanM" Format
# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
#
-# See also: http://www.fileformat.info/format/mspaint/egff.htm
+# See also: https://www.fileformat.info/format/mspaint/egff.htm
import io
import struct
@@ -73,7 +73,7 @@ class MspImageFile(ImageFile.ImageFile):
class MspDecoder(ImageFile.PyDecoder):
# The algo for the MSP decoder is from
- # http://www.fileformat.info/format/mspaint/egff.htm
+ # https://www.fileformat.info/format/mspaint/egff.htm
# cc-by-attribution -- That page references is taken from the
# Encyclopedia of Graphics File Formats and is licensed by
# O'Reilly under the Creative Common/Attribution license
diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py
index c1bd933d3..743c35f01 100644
--- a/src/PIL/PSDraw.py
+++ b/src/PIL/PSDraw.py
@@ -26,39 +26,36 @@ from . import EpsImagePlugin
class PSDraw:
"""
Sets up printing to the given file. If ``fp`` is omitted,
- :py:data:`sys.stdout` is assumed.
+ ``sys.stdout.buffer`` or ``sys.stdout`` is assumed.
"""
def __init__(self, fp=None):
if not fp:
- fp = sys.stdout
+ try:
+ fp = sys.stdout.buffer
+ except AttributeError:
+ fp = sys.stdout
self.fp = fp
- def _fp_write(self, to_write):
- if self.fp == sys.stdout:
- self.fp.write(to_write)
- else:
- self.fp.write(bytes(to_write, "UTF-8"))
-
def begin_document(self, id=None):
"""Set up printing of a document. (Write PostScript DSC header.)"""
# FIXME: incomplete
- self._fp_write(
- "%!PS-Adobe-3.0\n"
- "save\n"
- "/showpage { } def\n"
- "%%EndComments\n"
- "%%BeginDocument\n"
+ self.fp.write(
+ b"%!PS-Adobe-3.0\n"
+ b"save\n"
+ b"/showpage { } def\n"
+ b"%%EndComments\n"
+ b"%%BeginDocument\n"
)
- # self._fp_write(ERROR_PS) # debugging!
- self._fp_write(EDROFF_PS)
- self._fp_write(VDI_PS)
- self._fp_write("%%EndProlog\n")
+ # self.fp.write(ERROR_PS) # debugging!
+ self.fp.write(EDROFF_PS)
+ self.fp.write(VDI_PS)
+ self.fp.write(b"%%EndProlog\n")
self.isofont = {}
def end_document(self):
"""Ends printing. (Write PostScript DSC footer.)"""
- self._fp_write("%%EndDocument\nrestore showpage\n%%End\n")
+ self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n")
if hasattr(self.fp, "flush"):
self.fp.flush()
@@ -69,12 +66,13 @@ class PSDraw:
:param font: A PostScript font name
:param size: Size in points.
"""
+ font = bytes(font, "UTF-8")
if font not in self.isofont:
# reencode font
- self._fp_write(f"/PSDraw-{font} ISOLatin1Encoding /{font} E\n")
+ self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font))
self.isofont[font] = 1
# rough
- self._fp_write(f"/F0 {size} /PSDraw-{font} F\n")
+ self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font))
def line(self, xy0, xy1):
"""
@@ -82,7 +80,7 @@ class PSDraw:
PostScript point coordinates (72 points per inch, (0, 0) is the lower
left corner of the page).
"""
- self._fp_write("%d %d %d %d Vl\n" % (*xy0, *xy1))
+ self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1))
def rectangle(self, box):
"""
@@ -97,16 +95,18 @@ class PSDraw:
%d %d M %d %d 0 Vr\n
"""
- self._fp_write("%d %d M %d %d 0 Vr\n" % box)
+ self.fp.write(b"%d %d M %d %d 0 Vr\n" % box)
def text(self, xy, text):
"""
Draws text at the given position. You must use
:py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
"""
- text = "\\(".join(text.split("("))
- text = "\\)".join(text.split(")"))
- self._fp_write(f"{xy[0]} {xy[1]} M ({text}) S\n")
+ text = bytes(text, "UTF-8")
+ text = b"\\(".join(text.split(b"("))
+ text = b"\\)".join(text.split(b")"))
+ xy += (text,)
+ self.fp.write(b"%d %d M (%s) S\n" % xy)
def image(self, box, im, dpi=None):
"""Draw a PIL image, centered in the given box."""
@@ -130,14 +130,14 @@ class PSDraw:
y = ymax
dx = (xmax - x) / 2 + box[0]
dy = (ymax - y) / 2 + box[1]
- self._fp_write(f"gsave\n{dx:f} {dy:f} translate\n")
+ self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy))
if (x, y) != im.size:
# EpsImagePlugin._save prints the image at (0,0,xsize,ysize)
sx = x / im.size[0]
sy = y / im.size[1]
- self._fp_write(f"{sx:f} {sy:f} scale\n")
+ self.fp.write(b"%f %f scale\n" % (sx, sy))
EpsImagePlugin._save(im, self.fp, None, 0)
- self._fp_write("\ngrestore\n")
+ self.fp.write(b"\ngrestore\n")
# --------------------------------------------------------------------
@@ -153,7 +153,7 @@ class PSDraw:
#
-EDROFF_PS = """\
+EDROFF_PS = b"""\
/S { show } bind def
/P { moveto show } bind def
/M { moveto } bind def
@@ -182,7 +182,7 @@ EDROFF_PS = """\
# Copyright (c) Fredrik Lundh 1994.
#
-VDI_PS = """\
+VDI_PS = b"""\
/Vm { moveto } bind def
/Va { newpath arcn stroke } bind def
/Vl { moveto lineto stroke } bind def
@@ -207,7 +207,7 @@ VDI_PS = """\
# 89-11-21 fl: created (pslist 1.10)
#
-ERROR_PS = """\
+ERROR_PS = b"""\
/landscape false def
/errorBUF 200 string def
/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index 36c8fb849..1131c6325 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -124,7 +124,7 @@ def _save(im, fp, filename, save_all=False):
decode = None
if im.mode == "1":
- filter = "ASCIIHexDecode"
+ filter = "DCTDecode"
colorspace = PdfParser.PdfName("DeviceGray")
procset = "ImageB" # grayscale
bits = 1
@@ -135,7 +135,7 @@ def _save(im, fp, filename, save_all=False):
procset = "ImageB" # grayscale
elif im.mode == "P":
filter = "ASCIIHexDecode"
- palette = im.im.getpalette("RGB")
+ palette = im.getpalette()
colorspace = [
PdfParser.PdfName("Indexed"),
PdfParser.PdfName("DeviceRGB"),
@@ -161,12 +161,6 @@ def _save(im, fp, filename, save_all=False):
op = io.BytesIO()
if filter == "ASCIIHexDecode":
- if bits == 1:
- # FIXME: the hex encoder doesn't support packed 1-bit
- # images; do things the hard way...
- data = im.tobytes("raw", "1")
- im = Image.new("L", im.size)
- im.putdata(data)
ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
elif filter == "DCTDecode":
Image.SAVE["JPEG"](im, op, filename)
@@ -208,8 +202,8 @@ def _save(im, fp, filename, save_all=False):
MediaBox=[
0,
0,
- int(width * 72.0 / resolution),
- int(height * 72.0 / resolution),
+ width * 72.0 / resolution,
+ height * 72.0 / resolution,
],
Contents=contents_refs[pageNumber],
)
@@ -217,9 +211,9 @@ def _save(im, fp, filename, save_all=False):
#
# page contents
- page_contents = b"q %d 0 0 %d 0 0 cm /image Do Q\n" % (
- int(width * 72.0 / resolution),
- int(height * 72.0 / resolution),
+ page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
+ width * 72.0 / resolution,
+ height * 72.0 / resolution,
)
existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents)
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 86d78a95c..6ac9c7a7c 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -330,6 +330,8 @@ def pdf_repr(x):
return bytes(x)
elif isinstance(x, int):
return str(x).encode("us-ascii")
+ elif isinstance(x, float):
+ return str(x).encode("us-ascii")
elif isinstance(x, time.struct_time):
return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")"
elif isinstance(x, dict):
@@ -423,7 +425,7 @@ class PdfParser:
self.f.write(b"%PDF-1.4\n")
def write_comment(self, s):
- self.f.write(f"% {s}\n".encode("utf-8"))
+ self.f.write(f"% {s}\n".encode())
def write_catalog(self):
self.del_root()
@@ -580,7 +582,8 @@ class PdfParser:
whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]"
whitespace_optional = whitespace + b"*"
whitespace_mandatory = whitespace + b"+"
- whitespace_optional_no_nl = br"[\000\011\014\015\040]*" # no "\012" aka "\n"
+ # No "\012" aka "\n" or "\015" aka "\r":
+ whitespace_optional_no_nl = br"[\000\011\014\040]*"
newline_only = br"[\r\n]+"
newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl
re_trailer_end = re.compile(
@@ -860,7 +863,7 @@ class PdfParser:
if m:
# filter out whitespace
hex_string = bytearray(
- [b for b in m.group(1) if b in b"0123456789abcdefABCDEF"]
+ b for b in m.group(1) if b in b"0123456789abcdefABCDEF"
)
if len(hex_string) % 2 == 1:
# append a 0 if the length is not even - yes, at the end
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 07bbc5228..0f596f1fd 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -500,7 +500,7 @@ class PngStream(ChunkStream):
px, py = i32(s, 0), i32(s, 4)
unit = s[8]
if unit == 1: # meter
- dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5)
+ dpi = px * 0.0254, py * 0.0254
self.im_info["dpi"] = dpi
elif unit == 0:
self.im_info["aspect"] = px, py
@@ -920,6 +920,8 @@ class PngImageFile(ImageFile.ImageFile):
def load_end(self):
"""internal: finished reading image data"""
+ if self.__idat != 0:
+ self.fp.read(self.__idat)
while True:
self.fp.read(4) # CRC
@@ -976,6 +978,18 @@ class PngImageFile(ImageFile.ImageFile):
return super().getexif()
+ def getxmp(self):
+ """
+ Returns a dictionary containing the XMP tags.
+ Requires defusedxml to be installed.
+ :returns: XMP tags in a dictionary.
+ """
+ return (
+ self._getxmp(self.info["XML:com.adobe.xmp"])
+ if "XML:com.adobe.xmp" in self.info
+ else {}
+ )
+
def _close__fp(self):
try:
if self.__fp != self.fp:
@@ -1047,8 +1061,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
- disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
- blend = im.encoderinfo.get("blend", im.info.get("blend"))
+ disposal = im.encoderinfo.get(
+ "disposal", im.info.get("disposal", APNG_DISPOSE_OP_NONE)
+ )
+ blend = im.encoderinfo.get("blend", im.info.get("blend", APNG_BLEND_OP_SOURCE))
if default_image:
chain = itertools.chain(im.encoderinfo.get("append_images", []))
@@ -1103,12 +1119,8 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
and prev_disposal == encoderinfo.get("disposal")
and prev_blend == encoderinfo.get("blend")
):
- duration = encoderinfo.get("duration", 0)
- if duration:
- if "duration" in previous["encoderinfo"]:
- previous["encoderinfo"]["duration"] += duration
- else:
- previous["encoderinfo"]["duration"] = duration
+ if isinstance(duration, (list, tuple)):
+ previous["encoderinfo"]["duration"] += encoderinfo["duration"]
continue
else:
bbox = None
@@ -1135,9 +1147,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
bbox = frame_data["bbox"]
im_frame = im_frame.crop(bbox)
size = im_frame.size
- duration = int(round(frame_data["encoderinfo"].get("duration", 0)))
- disposal = frame_data["encoderinfo"].get("disposal", APNG_DISPOSE_OP_NONE)
- blend = frame_data["encoderinfo"].get("blend", APNG_BLEND_OP_SOURCE)
+ encoderinfo = frame_data["encoderinfo"]
+ frame_duration = int(round(encoderinfo.get("duration", duration)))
+ frame_disposal = encoderinfo.get("disposal", disposal)
+ frame_blend = encoderinfo.get("blend", blend)
# frame control
chunk(
fp,
@@ -1147,10 +1160,10 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
o32(size[1]), # height
o32(bbox[0]), # x_offset
o32(bbox[1]), # y_offset
- o16(duration), # delay_numerator
+ o16(frame_duration), # delay_numerator
o16(1000), # delay_denominator
- o8(disposal), # dispose_op
- o8(blend), # blend_op
+ o8(frame_disposal), # dispose_op
+ o8(frame_blend), # blend_op
)
seq_num += 1
# frame data
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index d3799edc3..04b21e3de 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i8
from ._binary import i16be as i16
from ._binary import i32be as i32
+from ._binary import si16be as si16
MODES = {
# (photoshop mode, bits) -> (pil mode, required channels)
@@ -119,7 +120,8 @@ class PsdImageFile(ImageFile.ImageFile):
end = self.fp.tell() + size
size = i32(read(4))
if size:
- self.layers = _layerinfo(self.fp)
+ _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size))
+ self.layers = _layerinfo(_layer_data, size)
self.fp.seek(end)
self.n_frames = len(self.layers)
self.is_animated = self.n_frames > 1
@@ -171,11 +173,20 @@ class PsdImageFile(ImageFile.ImageFile):
self.__fp = None
-def _layerinfo(file):
+def _layerinfo(fp, ct_bytes):
# read layerinfo block
layers = []
- read = file.read
- for i in range(abs(i16(read(2)))):
+
+ def read(size):
+ return ImageFile._safe_read(fp, size)
+
+ ct = si16(read(2))
+
+ # sanity check
+ if ct_bytes < (abs(ct) * 20):
+ raise SyntaxError("Layer block too short for number of layers requested")
+
+ for i in range(abs(ct)):
# bounding box
y0 = i32(read(4))
@@ -186,7 +197,8 @@ def _layerinfo(file):
# image info
info = []
mode = []
- types = list(range(i16(read(2))))
+ ct_types = i16(read(2))
+ types = list(range(ct_types))
if len(types) > 4:
continue
@@ -219,16 +231,16 @@ def _layerinfo(file):
size = i32(read(4)) # length of the extra data field
combined = 0
if size:
- data_end = file.tell() + size
+ data_end = fp.tell() + size
length = i32(read(4))
if length:
- file.seek(length - 16, io.SEEK_CUR)
+ fp.seek(length - 16, io.SEEK_CUR)
combined += length + 4
length = i32(read(4))
if length:
- file.seek(length, io.SEEK_CUR)
+ fp.seek(length, io.SEEK_CUR)
combined += length + 4
length = i8(read(1))
@@ -238,7 +250,7 @@ def _layerinfo(file):
name = read(length).decode("latin-1", "replace")
combined += length + 1
- file.seek(data_end)
+ fp.seek(data_end)
layers.append((name, mode, (x0, y0, x1, y1)))
# get tiles
@@ -246,7 +258,7 @@ def _layerinfo(file):
for name, mode, bbox in layers:
tile = []
for m in mode:
- t = _maketile(file, m, bbox, 1)
+ t = _maketile(fp, m, bbox, 1)
if t:
tile.extend(t)
layers[i] = name, mode, bbox, tile
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
index 494f5f9f4..eeaa0ccc4 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -54,6 +54,7 @@ class PyAccess:
self.image32 = ffi.cast("int **", vals["image32"])
self.image = ffi.cast("unsigned char **", vals["image"])
self.xsize, self.ysize = img.im.size
+ self._img = img
# Keep pointer to im object to prevent dereferencing.
self._im = img.im
@@ -93,7 +94,7 @@ class PyAccess:
and len(color) in [3, 4]
):
# RGB or RGBA value for a P image
- color = self._palette.getcolor(color)
+ color = self._palette.getcolor(color, self._img)
return self.set_pixel(x, y, color)
@@ -127,7 +128,7 @@ class PyAccess:
class _PyAccess32_2(PyAccess):
- """ PA, LA, stored in first and last bytes of a 32 bit word """
+ """PA, LA, stored in first and last bytes of a 32 bit word"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
@@ -144,7 +145,7 @@ class _PyAccess32_2(PyAccess):
class _PyAccess32_3(PyAccess):
- """ RGB and friends, stored in the first three bytes of a 32 bit word """
+ """RGB and friends, stored in the first three bytes of a 32 bit word"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
@@ -163,7 +164,7 @@ class _PyAccess32_3(PyAccess):
class _PyAccess32_4(PyAccess):
- """ RGBA etc, all 4 bytes of a 32 bit word """
+ """RGBA etc, all 4 bytes of a 32 bit word"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32)
@@ -182,7 +183,7 @@ class _PyAccess32_4(PyAccess):
class _PyAccess8(PyAccess):
- """ 1, L, P, 8 bit images stored as uint8 """
+ """1, L, P, 8 bit images stored as uint8"""
def _post_init(self, *args, **kwargs):
self.pixels = self.image8
@@ -200,7 +201,7 @@ class _PyAccess8(PyAccess):
class _PyAccessI16_N(PyAccess):
- """ I;16 access, native bitendian without conversion """
+ """I;16 access, native bitendian without conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("unsigned short **", self.image)
@@ -218,7 +219,7 @@ class _PyAccessI16_N(PyAccess):
class _PyAccessI16_L(PyAccess):
- """ I;16L access, with conversion """
+ """I;16L access, with conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
@@ -239,7 +240,7 @@ class _PyAccessI16_L(PyAccess):
class _PyAccessI16_B(PyAccess):
- """ I;16B access, with conversion """
+ """I;16B access, with conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("struct Pixel_I16 **", self.image)
@@ -260,7 +261,7 @@ class _PyAccessI16_B(PyAccess):
class _PyAccessI32_N(PyAccess):
- """ Signed Int32 access, native endian """
+ """Signed Int32 access, native endian"""
def _post_init(self, *args, **kwargs):
self.pixels = self.image32
@@ -273,7 +274,7 @@ class _PyAccessI32_N(PyAccess):
class _PyAccessI32_Swap(PyAccess):
- """ I;32L/B access, with byteswapping conversion """
+ """I;32L/B access, with byteswapping conversion"""
def _post_init(self, *args, **kwargs):
self.pixels = self.image32
@@ -292,7 +293,7 @@ class _PyAccessI32_Swap(PyAccess):
class _PyAccessF(PyAccess):
- """ 32 bit float access """
+ """32 bit float access"""
def _post_init(self, *args, **kwargs):
self.pixels = ffi.cast("float **", self.image32)
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index d0f7c9993..5f1ef6edc 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -193,7 +193,8 @@ def _save(im, fp, filename):
for channel in im.split():
fp.write(channel.tobytes("raw", rawmode, 0, orientation))
- fp.close()
+ if hasattr(fp, "flush"):
+ fp.flush()
class SGI16Decoder(ImageFile.PyDecoder):
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 819f2ed0a..062af9f98 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -296,7 +296,7 @@ Image.register_save(SpiderImageFile.format, _save_spider)
if __name__ == "__main__":
if len(sys.argv) < 2:
- print("Syntax: python SpiderImagePlugin.py [infile] [outfile]")
+ print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]")
sys.exit()
filename = sys.argv[1]
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 2b936d687..ed63da95f 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -93,9 +93,10 @@ class TgaImageFile(ImageFile.ImageFile):
# orientation
orientation = flags & 0x30
- if orientation == 0x20:
+ self._flip_horizontally = orientation in [0x10, 0x30]
+ if orientation in [0x20, 0x30]:
orientation = 1
- elif not orientation:
+ elif orientation in [0, 0x10]:
orientation = -1
else:
raise SyntaxError("unknown TGA orientation")
@@ -110,10 +111,10 @@ class TgaImageFile(ImageFile.ImageFile):
if colormaptype:
# read palette
- start, size, mapdepth = i16(s, 3), i16(s, 5), i16(s, 7)
+ start, size, mapdepth = i16(s, 3), i16(s, 5), s[7]
if mapdepth == 16:
self.palette = ImagePalette.raw(
- "BGR;16", b"\0" * 2 * start + self.fp.read(2 * size)
+ "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size)
)
elif mapdepth == 24:
self.palette = ImagePalette.raw(
@@ -149,6 +150,10 @@ class TgaImageFile(ImageFile.ImageFile):
except KeyError:
pass # cannot decode
+ def load_end(self):
+ if self._flip_horizontally:
+ self.im = self.im.transpose(Image.FLIP_LEFT_RIGHT)
+
#
# --------------------------------------------------------------------
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 24821d130..5df5c4f4c 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -48,7 +48,7 @@ from collections.abc import MutableMapping
from fractions import Fraction
from numbers import Number, Rational
-from . import Image, ImageFile, ImagePalette, TiffTags
+from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import o8
from .TiffTags import TYPES
@@ -58,6 +58,7 @@ logger = logging.getLogger(__name__)
READ_LIBTIFF = False
WRITE_LIBTIFF = False
IFD_LEGACY_API = True
+STRIP_SIZE = 65536
II = b"II" # little-endian (Intel style)
MM = b"MM" # big-endian (Motorola style)
@@ -88,11 +89,15 @@ DATE_TIME = 306
ARTIST = 315
PREDICTOR = 317
COLORMAP = 320
+TILEWIDTH = 322
+TILELENGTH = 323
TILEOFFSETS = 324
+TILEBYTECOUNTS = 325
SUBIFD = 330
EXTRASAMPLES = 338
SAMPLEFORMAT = 339
JPEGTABLES = 347
+YCBCRSUBSAMPLING = 530
REFERENCEBLACKWHITE = 532
COPYRIGHT = 33432
IPTC_NAA_CHUNK = 33723 # newsphoto properties
@@ -354,9 +359,22 @@ class IFDRational(Rational):
return self._val.__hash__()
def __eq__(self, other):
+ val = self._val
if isinstance(other, IFDRational):
other = other._val
- return self._val == other
+ if isinstance(other, float):
+ val = float(val)
+ return val == other
+
+ def __getstate__(self):
+ return [self._val, self._numerator, self._denominator]
+
+ def __setstate__(self, state):
+ IFDRational.__init__(self, 0)
+ _val, _numerator, _denominator = state
+ self._val = _val
+ self._numerator = _numerator
+ self._denominator = _denominator
def _delegate(op):
def delegate(self, *args):
@@ -423,39 +441,45 @@ class ImageFileDirectory_v2(MutableMapping):
Data Structures:
- * self.tagtype = {}
+ * ``self.tagtype = {}``
- * Key: numerical tiff tag number
+ * Key: numerical TIFF tag number
* Value: integer corresponding to the data type from
- ~PIL.TiffTags.TYPES`
+ :py:data:`.TiffTags.TYPES`
- .. versionadded:: 3.0.0
- """
+ .. versionadded:: 3.0.0
- """
- Documentation:
+ 'Internal' data structures:
- 'internal' data structures:
- * self._tags_v2 = {} Key: numerical tiff tag number
- Value: decoded data, as tuple for multiple values
- * self._tagdata = {} Key: numerical tiff tag number
- Value: undecoded byte string from file
- * self._tags_v1 = {} Key: numerical tiff tag number
- Value: decoded data in the v1 format
+ * ``self._tags_v2 = {}``
- Tags will be found in the private attributes self._tagdata, and in
- self._tags_v2 once decoded.
+ * Key: numerical TIFF tag number
+ * Value: decoded data, as tuple for multiple values
- Self.legacy_api is a value for internal use, and shouldn't be
- changed from outside code. In cooperation with the
- ImageFileDirectory_v1 class, if legacy_api is true, then decoded
- tags will be populated into both _tags_v1 and _tags_v2. _Tags_v2
- will be used if this IFD is used in the TIFF save routine. Tags
- should be read from tags_v1 if legacy_api == true.
+ * ``self._tagdata = {}``
+
+ * Key: numerical TIFF tag number
+ * Value: undecoded byte string from file
+
+ * ``self._tags_v1 = {}``
+
+ * Key: numerical TIFF tag number
+ * Value: decoded data in the v1 format
+
+ Tags will be found in the private attributes ``self._tagdata``, and in
+ ``self._tags_v2`` once decoded.
+
+ ``self.legacy_api`` is a value for internal use, and shouldn't be changed
+ from outside code. In cooperation with
+ :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``
+ is true, then decoded tags will be populated into both ``_tags_v1`` and
+ ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF
+ save routine. Tags should be read from ``_tags_v1`` if
+ ``legacy_api == true``.
"""
- def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None):
+ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
"""Initialize an ImageFileDirectory.
To construct an ImageFileDirectory from a real file, pass the 8-byte
@@ -475,6 +499,7 @@ class ImageFileDirectory_v2(MutableMapping):
self._endian = "<"
else:
raise SyntaxError("not a TIFF IFD")
+ self.group = group
self.tagtype = {}
""" Dictionary of tag types """
self.reset()
@@ -506,7 +531,10 @@ class ImageFileDirectory_v2(MutableMapping):
Returns the complete tag dictionary, with named tags where possible.
"""
- return {TiffTags.lookup(code).name: value for code, value in self.items()}
+ return {
+ TiffTags.lookup(code, self.group).name: value
+ for code, value in self.items()
+ }
def __len__(self):
return len(set(self._tagdata) | set(self._tags_v2))
@@ -531,7 +559,7 @@ class ImageFileDirectory_v2(MutableMapping):
def _setitem(self, tag, value, legacy_api):
basetypes = (Number, bytes, str)
- info = TiffTags.lookup(tag)
+ info = TiffTags.lookup(tag, self.group)
values = [value] if isinstance(value, basetypes) else value
if tag not in self.tagtype:
@@ -565,7 +593,8 @@ class ImageFileDirectory_v2(MutableMapping):
if self.tagtype[tag] == TiffTags.UNDEFINED:
values = [
- value.encode("ascii", "replace") if isinstance(value, str) else value
+ v.encode("ascii", "replace") if isinstance(v, str) else v
+ for v in values
]
elif self.tagtype[tag] == TiffTags.RATIONAL:
values = [float(v) if isinstance(v, int) else v for v in values]
@@ -648,7 +677,7 @@ class ImageFileDirectory_v2(MutableMapping):
_load_dispatch[idx] = ( # noqa: F821
size,
lambda self, data, legacy_api=True: (
- self._unpack("{}{}".format(len(data) // size, fmt), data)
+ self._unpack(f"{len(data) // size}{fmt}", data)
),
)
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821
@@ -692,7 +721,7 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_loader(5, 8)
def load_rational(self, data, legacy_api=True):
- vals = self._unpack("{}L".format(len(data) // 4), data)
+ vals = self._unpack(f"{len(data) // 4}L", data)
def combine(a, b):
return (a, b) if legacy_api else IFDRational(a, b)
@@ -715,7 +744,7 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_loader(10, 8)
def load_signed_rational(self, data, legacy_api=True):
- vals = self._unpack("{}l".format(len(data) // 4), data)
+ vals = self._unpack(f"{len(data) // 4}l", data)
def combine(a, b):
return (a, b) if legacy_api else IFDRational(a, b)
@@ -747,7 +776,7 @@ class ImageFileDirectory_v2(MutableMapping):
for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]):
tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12))
- tagname = TiffTags.lookup(tag).name
+ tagname = TiffTags.lookup(tag, self.group).name
typname = TYPES.get(typ, "unknown")
msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"
@@ -814,15 +843,16 @@ class ImageFileDirectory_v2(MutableMapping):
ifh = b"II\x2A\x00\x08\x00\x00\x00"
else:
ifh = b"MM\x00\x2A\x00\x00\x00\x08"
- ifd = ImageFileDirectory_v2(ifh)
- for ifd_tag, ifd_value in self._tags_v2[tag].items():
+ ifd = ImageFileDirectory_v2(ifh, group=tag)
+ values = self._tags_v2[tag]
+ for ifd_tag, ifd_value in values.items():
ifd[ifd_tag] = ifd_value
data = ifd.tobytes(offset)
else:
values = value if isinstance(value, tuple) else (value,)
data = self._write_dispatch[typ](self, *values)
- tagname = TiffTags.lookup(tag).name
+ tagname = TiffTags.lookup(tag, self.group).name
typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})"
msg += " - value: " + (
@@ -1052,6 +1082,11 @@ class TiffImageFile(ImageFile.ImageFile):
def _seek(self, frame):
self.fp = self.__fp
+
+ # reset buffered io handle in case fp
+ # was passed to libtiff, invalidating the buffer
+ self.fp.tell()
+
while len(self._frame_pos) <= frame:
if not self.__next:
raise EOFError("no more images in TIFF file")
@@ -1059,14 +1094,16 @@ class TiffImageFile(ImageFile.ImageFile):
f"Seeking to frame {frame}, on frame {self.__frame}, "
f"__next {self.__next}, location: {self.fp.tell()}"
)
- # reset buffered io handle in case fp
- # was passed to libtiff, invalidating the buffer
- self.fp.tell()
self.fp.seek(self.__next)
self._frame_pos.append(self.__next)
logger.debug("Loading tags, location: %s" % self.fp.tell())
self.tag_v2.load(self.fp)
- self.__next = self.tag_v2.next
+ if self.tag_v2.next in self._frame_pos:
+ # This IFD has already been processed
+ # Declare this to be the end of the image
+ self.__next = 0
+ else:
+ self.__next = self.tag_v2.next
if self.__next == 0:
self._n_frames = frame + 1
if len(self._frame_pos) == 1:
@@ -1083,6 +1120,14 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
+ def getxmp(self):
+ """
+ Returns a dictionary containing the XMP tags.
+ Requires defusedxml to be installed.
+ :returns: XMP tags in a dictionary.
+ """
+ return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {}
+
def load(self):
if self.tile and self.use_load_libtiff:
return self._load_libtiff()
@@ -1108,6 +1153,17 @@ class TiffImageFile(ImageFile.ImageFile):
if not self.is_animated:
self._close_exclusive_fp_after_loading = True
+ # reset buffered io handle in case fp
+ # was passed to libtiff, invalidating the buffer
+ self.fp.tell()
+
+ # load IFD data from fp before it is closed
+ exif = self.getexif()
+ for key in TiffTags.TAGS_V2_GROUPS.keys():
+ if key not in exif:
+ continue
+ exif.get_ifd(key)
+
def _load_libtiff(self):
"""Overload method triggered when we detect a compressed tiff
Calls out to libtiff"""
@@ -1250,6 +1306,13 @@ class TiffImageFile(ImageFile.ImageFile):
if bps_count > len(bps_tuple) and len(bps_tuple) == 1:
bps_tuple = bps_tuple * bps_count
+ samplesPerPixel = self.tag_v2.get(
+ SAMPLESPERPIXEL,
+ 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
+ )
+ if len(bps_tuple) != samplesPerPixel:
+ raise SyntaxError("unknown data organization")
+
# mode: check photometric interpretation and bits per pixel
key = (
self.tag_v2.prefix,
@@ -1277,11 +1340,11 @@ class TiffImageFile(ImageFile.ImageFile):
if xres and yres:
resunit = self.tag_v2.get(RESOLUTION_UNIT)
if resunit == 2: # dots per inch
- self.info["dpi"] = int(xres + 0.5), int(yres + 0.5)
+ self.info["dpi"] = (xres, yres)
elif resunit == 3: # dots per centimeter. convert to dpi
- self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5)
+ self.info["dpi"] = (xres * 2.54, yres * 2.54)
elif resunit is None: # used to default to 1, but now 2)
- self.info["dpi"] = int(xres + 0.5), int(yres + 0.5)
+ self.info["dpi"] = (xres, yres)
# For backward compatibility,
# we also preserve the old behavior
self.info["resolution"] = xres, yres
@@ -1445,7 +1508,9 @@ def _save(im, fp, filename):
ifd = ImageFileDirectory_v2(prefix=prefix)
- compression = im.encoderinfo.get("compression", im.info.get("compression"))
+ encoderinfo = im.encoderinfo
+ encoderconfig = im.encoderconfig
+ compression = encoderinfo.get("compression", im.info.get("compression"))
if compression is None:
compression = "raw"
elif compression == "tiff_jpeg":
@@ -1463,12 +1528,24 @@ def _save(im, fp, filename):
ifd[IMAGELENGTH] = im.size[1]
# write any arbitrary tags passed in as an ImageFileDirectory
- info = im.encoderinfo.get("tiffinfo", {})
+ if "tiffinfo" in encoderinfo:
+ info = encoderinfo["tiffinfo"]
+ elif "exif" in encoderinfo:
+ info = encoderinfo["exif"]
+ if isinstance(info, bytes):
+ exif = Image.Exif()
+ exif.load(info)
+ info = exif
+ else:
+ info = {}
logger.debug("Tiffinfo Keys: %s" % list(info))
if isinstance(info, ImageFileDirectory_v1):
info = info.to_v2()
for key in info:
- ifd[key] = info.get(key)
+ if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():
+ ifd[key] = info.get_ifd(key)
+ else:
+ ifd[key] = info.get(key)
try:
ifd.tagtype[key] = info.tagtype[key]
except Exception:
@@ -1492,7 +1569,7 @@ def _save(im, fp, filename):
# preserve ICC profile (should also work when saving other formats
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech
- icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
+ icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
if icc:
ifd[ICCPROFILE] = icc
@@ -1508,14 +1585,14 @@ def _save(im, fp, filename):
(ARTIST, "artist"),
(COPYRIGHT, "copyright"),
]:
- if name in im.encoderinfo:
- ifd[key] = im.encoderinfo[name]
+ if name in encoderinfo:
+ ifd[key] = encoderinfo[name]
- dpi = im.encoderinfo.get("dpi")
+ dpi = encoderinfo.get("dpi")
if dpi:
ifd[RESOLUTION_UNIT] = 2
- ifd[X_RESOLUTION] = int(dpi[0] + 0.5)
- ifd[Y_RESOLUTION] = int(dpi[1] + 0.5)
+ ifd[X_RESOLUTION] = dpi[0]
+ ifd[Y_RESOLUTION] = dpi[1]
if bits != (1,):
ifd[BITSPERSAMPLE] = bits
@@ -1526,25 +1603,59 @@ def _save(im, fp, filename):
if format != 1:
ifd[SAMPLEFORMAT] = format
- ifd[PHOTOMETRIC_INTERPRETATION] = photo
+ if PHOTOMETRIC_INTERPRETATION not in ifd:
+ ifd[PHOTOMETRIC_INTERPRETATION] = photo
+ elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
+ if im.mode == "1":
+ inverted_im = im.copy()
+ px = inverted_im.load()
+ for y in range(inverted_im.height):
+ for x in range(inverted_im.width):
+ px[x, y] = 0 if px[x, y] == 255 else 255
+ im = inverted_im
+ else:
+ im = ImageOps.invert(im)
if im.mode in ["P", "PA"]:
lut = im.im.getpalette("RGB", "RGB;L")
ifd[COLORMAP] = tuple(v * 256 for v in lut)
# data orientation
stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
- ifd[ROWSPERSTRIP] = im.size[1]
- strip_byte_counts = stride * im.size[1]
+ # aim for given strip size (64 KB by default) when using libtiff writer
+ if libtiff:
+ rows_per_strip = 1 if stride == 0 else min(STRIP_SIZE // stride, im.size[1])
+ # JPEG encoder expects multiple of 8 rows
+ if compression == "jpeg":
+ rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
+ else:
+ rows_per_strip = im.size[1]
+ if rows_per_strip == 0:
+ rows_per_strip = 1
+ strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
+ strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
+ ifd[ROWSPERSTRIP] = rows_per_strip
if strip_byte_counts >= 2 ** 16:
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
- ifd[STRIPBYTECOUNTS] = strip_byte_counts
- ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
+ ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
+ stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
+ )
+ ifd[STRIPOFFSETS] = tuple(
+ range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
+ ) # this is adjusted by IFD writer
# no compression by default:
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
+ if im.mode == "YCbCr":
+ for tag, value in {
+ YCBCRSUBSAMPLING: (1, 1),
+ REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
+ }.items():
+ ifd.setdefault(tag, value)
+
+ blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
if libtiff:
- if "quality" in im.encoderinfo:
- quality = im.encoderinfo["quality"]
+ if "quality" in encoderinfo:
+ quality = encoderinfo["quality"]
if not isinstance(quality, int) or quality < 0 or quality > 100:
raise ValueError("Invalid quality setting")
if compression != "jpeg":
@@ -1565,17 +1676,14 @@ def _save(im, fp, filename):
# optional types for non core tags
types = {}
- # SAMPLEFORMAT is determined by the image format and should not be copied
- # from legacy_ifd.
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
# based on the data in the strip.
# The other tags expect arrays with a certain length (fixed or depending on
# BITSPERSAMPLE, etc), passing arrays with a different length will result in
# segfaults. Block these tags until we add extra validation.
# SUBIFD may also cause a segfault.
- blocklist = [
+ blocklist += [
REFERENCEBLACKWHITE,
- SAMPLEFORMAT,
STRIPBYTECOUNTS,
STRIPOFFSETS,
TRANSFERFUNCTION,
@@ -1591,9 +1699,14 @@ def _save(im, fp, filename):
legacy_ifd = {}
if hasattr(im, "tag"):
legacy_ifd = im.tag.to_v2()
- for tag, value in itertools.chain(
- ifd.items(), getattr(im, "tag_v2", {}).items(), legacy_ifd.items()
- ):
+
+ # SAMPLEFORMAT is determined by the image format and should not be copied
+ # from legacy_ifd.
+ supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd}
+ if SAMPLEFORMAT in supplied_tags:
+ del supplied_tags[SAMPLEFORMAT]
+
+ for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
# Libtiff can only process certain core items without adding
# them to the custom dictionary.
# Custom items are supported for int, float, unicode, string and byte
@@ -1618,6 +1731,9 @@ def _save(im, fp, filename):
else:
atts[tag] = value
+ if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
+ atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
+
logger.debug("Converted items: %s" % sorted(atts.items()))
# libtiff always expects the bytes in native order.
@@ -1633,7 +1749,7 @@ def _save(im, fp, filename):
tags = list(atts.items())
tags.sort()
a = (rawmode, compression, _fp, filename, tags, types)
- e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig)
+ e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
e.setimage(im.im, (0, 0) + im.size)
while True:
# undone, change to self.decodermaxblock:
@@ -1646,6 +1762,8 @@ def _save(im, fp, filename):
raise OSError(f"encoder error {s} when writing image file")
else:
+ for tag in blocklist:
+ del ifd[tag]
offset = ifd.save(fp)
ImageFile._save(
@@ -1653,7 +1771,7 @@ def _save(im, fp, filename):
)
# -- helper for multi-page save --
- if "_debug_multipage" in im.encoderinfo:
+ if "_debug_multipage" in encoderinfo:
# just to access o32 and o16 (using correct byte order)
im._debug_multipage = ifd
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 9e9e117a4..88856aa92 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -33,7 +33,7 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")):
return self.enum.get(value, value) if self.enum else value
-def lookup(tag):
+def lookup(tag, group=None):
"""
:param tag: Integer tag number
:returns: Taginfo namedtuple, From the TAGS_V2 info if possible,
@@ -42,7 +42,11 @@ def lookup(tag):
"""
- return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, "unknown")))
+ if group is not None:
+ info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None
+ else:
+ info = TAGS_V2.get(tag)
+ return info or TagInfo(tag, TAGS.get(tag, "unknown"))
##
@@ -178,13 +182,15 @@ TAGS_V2 = {
532: ("ReferenceBlackWhite", RATIONAL, 6),
700: ("XMP", BYTE, 0),
33432: ("Copyright", ASCII, 1),
- 33723: ("IptcNaaInfo", UNDEFINED, 0),
+ 33723: ("IptcNaaInfo", UNDEFINED, 1),
34377: ("PhotoshopInfo", BYTE, 0),
# FIXME add more tags here
34665: ("ExifIFD", LONG, 1),
34675: ("ICCProfile", UNDEFINED, 1),
34853: ("GPSInfoIFD", LONG, 1),
+ 36864: ("ExifVersion", UNDEFINED, 1),
40965: ("InteroperabilityIFD", LONG, 1),
+ 41730: ("CFAPattern", UNDEFINED, 1),
# MPInfo
45056: ("MPFVersion", UNDEFINED, 1),
45057: ("NumberOfImages", LONG, 1),
@@ -205,11 +211,25 @@ TAGS_V2 = {
45579: ("YawAngle", SIGNED_RATIONAL, 1),
45580: ("PitchAngle", SIGNED_RATIONAL, 1),
45581: ("RollAngle", SIGNED_RATIONAL, 1),
+ 40960: ("FlashPixVersion", UNDEFINED, 1),
50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}),
50780: ("BestQualityScale", RATIONAL, 1),
50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one
50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006
}
+TAGS_V2_GROUPS = {
+ # ExifIFD
+ 34665: {
+ 36864: ("ExifVersion", UNDEFINED, 1),
+ 40960: ("FlashPixVersion", UNDEFINED, 1),
+ 40965: ("InteroperabilityIFD", LONG, 1),
+ 41730: ("CFAPattern", UNDEFINED, 1),
+ },
+ # GPSInfoIFD
+ 34853: {},
+ # InteroperabilityIFD
+ 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)},
+}
# Legacy Tags structure
# these tags aren't included above, but were in the previous versions
@@ -368,6 +388,10 @@ def _populate():
TAGS_V2[k] = TagInfo(k, *v)
+ for group, tags in TAGS_V2_GROUPS.items():
+ for k, v in tags.items():
+ tags[k] = TagInfo(k, *v)
+
_populate()
##
@@ -485,9 +509,6 @@ LIBTIFF_CORE = {
65537,
}
-LIBTIFF_CORE.remove(301) # Array of short, crashes
-LIBTIFF_CORE.remove(532) # Array of long, crashes
-
LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes
LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff
LIBTIFF_CORE.remove(323) # Tiled images
diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py
index b578d6981..1354ad32b 100644
--- a/src/PIL/WalImageFile.py
+++ b/src/PIL/WalImageFile.py
@@ -23,12 +23,44 @@ and has been tested with a few sample files found using google.
To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
"""
-import builtins
-
-from . import Image
+from . import Image, ImageFile
from ._binary import i32le as i32
+class WalImageFile(ImageFile.ImageFile):
+
+ format = "WAL"
+ format_description = "Quake2 Texture"
+
+ def _open(self):
+ self.mode = "P"
+
+ # read header fields
+ header = self.fp.read(32 + 24 + 32 + 12)
+ self._size = i32(header, 32), i32(header, 36)
+ Image._decompression_bomb_check(self.size)
+
+ # load pixel data
+ offset = i32(header, 40)
+ self.fp.seek(offset)
+
+ # strings are null-terminated
+ self.info["name"] = header[:32].split(b"\0", 1)[0]
+ next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
+ if next_name:
+ self.info["next_name"] = next_name
+
+ def load(self):
+ if self.im:
+ # Already loaded
+ return
+
+ self.im = Image.core.new(self.mode, self.size)
+ self.frombytes(self.fp.read(self.size[0] * self.size[1]))
+ self.putpalette(quake2palette)
+ Image.Image.load(self)
+
+
def open(filename):
"""
Load texture from a Quake2 WAL texture file.
@@ -39,38 +71,7 @@ def open(filename):
:param filename: WAL file name, or an opened file handle.
:returns: An image instance.
"""
- # FIXME: modify to return a WalImageFile instance instead of
- # plain Image object ?
-
- def imopen(fp):
- # read header fields
- header = fp.read(32 + 24 + 32 + 12)
- size = i32(header, 32), i32(header, 36)
- offset = i32(header, 40)
-
- # load pixel data
- fp.seek(offset)
-
- Image._decompression_bomb_check(size)
- im = Image.frombytes("P", size, fp.read(size[0] * size[1]))
- im.putpalette(quake2palette)
-
- im.format = "WAL"
- im.format_description = "Quake2 Texture"
-
- # strings are null-terminated
- im.info["name"] = header[:32].split(b"\0", 1)[0]
- next_name = header[56 : 56 + 32].split(b"\0", 1)[0]
- if next_name:
- im.info["next_name"] = next_name
-
- return im
-
- if hasattr(filename, "read"):
- return imopen(filename)
- else:
- with builtins.open(filename, "rb") as fp:
- return imopen(fp)
+ return WalImageFile(filename)
quake2palette = (
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index dfc8351ef..590161f3e 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -202,7 +202,7 @@ def _save_all(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
method = im.encoderinfo.get("method", 0)
- icc_profile = im.encoderinfo.get("icc_profile", "")
+ icc_profile = im.encoderinfo.get("icc_profile") or ""
exif = im.encoderinfo.get("exif", "")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
@@ -309,18 +309,18 @@ def _save_all(im, fp, filename):
def _save(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
- icc_profile = im.encoderinfo.get("icc_profile", "")
+ icc_profile = im.encoderinfo.get("icc_profile") or ""
exif = im.encoderinfo.get("exif", "")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
xmp = im.encoderinfo.get("xmp", "")
- method = im.encoderinfo.get("method", 0)
+ method = im.encoderinfo.get("method", 4)
if im.mode not in _VALID_WEBP_LEGACY_MODES:
alpha = (
"A" in im.mode
or "a" in im.mode
- or (im.mode == "P" and "A" in im.im.getpalettemode())
+ or (im.mode == "P" and "transparency" in im.info)
)
im = im.convert("RGBA" if alpha else "RGB")
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index 87847a107..27f5d2f87 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -127,8 +127,8 @@ class WmfStubImageFile(ImageFile.StubImageFile):
size = x1 - x0, y1 - y0
# calculate dots per inch from bbox and frame
- xdpi = int(2540.0 * (x1 - y0) / (frame[2] - frame[0]) + 0.5)
- ydpi = int(2540.0 * (y1 - y0) / (frame[3] - frame[1]) + 0.5)
+ xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0])
+ ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1])
self.info["wmf_bbox"] = x0, y0, x1, y1
@@ -152,7 +152,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def load(self, dpi=None):
if dpi is not None and self._inch is not None:
- self.info["dpi"] = int(dpi + 0.5)
+ self.info["dpi"] = dpi
x0, y0, x1, y1 = self.info["wmf_bbox"]
self._size = (
(x1 - x0) * self.info["dpi"] // self._inch,
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
index 890ae44f5..45fef241e 100644
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -13,72 +13,12 @@ Use PIL.__version__ for this Pillow version.
;-)
"""
-import sys
-import warnings
-
from . import _version
# VERSION was removed in Pillow 6.0.0.
-__version__ = _version.__version__
-
-
-# PILLOW_VERSION is deprecated and will be removed in a future release.
+# PILLOW_VERSION was removed in Pillow 9.0.0.
# Use __version__ instead.
-def _raise_version_warning():
- warnings.warn(
- "PILLOW_VERSION is deprecated and will be removed in Pillow 9 (2022-01-02). "
- "Use __version__ instead.",
- DeprecationWarning,
- stacklevel=3,
- )
-
-
-if sys.version_info >= (3, 7):
-
- def __getattr__(name):
- if name == "PILLOW_VERSION":
- _raise_version_warning()
- return __version__
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
-
-
-else:
-
- class _Deprecated_Version(str):
- def __str__(self):
- _raise_version_warning()
- return super().__str__()
-
- def __getitem__(self, key):
- _raise_version_warning()
- return super().__getitem__(key)
-
- def __eq__(self, other):
- _raise_version_warning()
- return super().__eq__(other)
-
- def __ne__(self, other):
- _raise_version_warning()
- return super().__ne__(other)
-
- def __gt__(self, other):
- _raise_version_warning()
- return super().__gt__(other)
-
- def __lt__(self, other):
- _raise_version_warning()
- return super().__lt__(other)
-
- def __ge__(self, other):
- _raise_version_warning()
- return super().__gt__(other)
-
- def __le__(self, other):
- _raise_version_warning()
- return super().__lt__(other)
-
- PILLOW_VERSION = _Deprecated_Version(__version__)
-
+__version__ = _version.__version__
del _version
diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py
index 5564f450d..a74ee9eb6 100644
--- a/src/PIL/_binary.py
+++ b/src/PIL/_binary.py
@@ -47,6 +47,16 @@ def si16le(c, o=0):
return unpack_from("h", c, o)[0]
+
+
def i32le(c, o=0):
"""
Converts a 4-bytes (32 bits) string to an unsigned integer.
diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py
index 58aeffbfb..ba4d045e6 100644
--- a/src/PIL/_tkinter_finder.py
+++ b/src/PIL/_tkinter_finder.py
@@ -14,7 +14,7 @@ tk_version = str(tkinter.TkVersion)
if tk_version == "8.4":
warnings.warn(
"Support for Tk/Tcl 8.4 is deprecated and will be removed"
- " in Pillow 10 (2023-01-02). Please upgrade to Tk/Tcl 8.5 "
+ " in Pillow 10 (2023-07-01). Please upgrade to Tk/Tcl 8.5 "
"or newer.",
DeprecationWarning,
)
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 20e8754a4..62c7dba55 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,2 +1,2 @@
# Master version for Pillow
-__version__ = "8.2.0.dev0"
+__version__ = "9.1.0.dev0"
diff --git a/src/PIL/features.py b/src/PIL/features.py
index 66d0ba10a..3838568f3 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -218,7 +218,7 @@ def get_supported():
def pilinfo(out=None, supported_formats=True):
"""
Prints information about this installation of Pillow.
- This function can be called with ``python -m PIL``.
+ This function can be called with ``python3 -m PIL``.
:param out:
The output stream to print to. Defaults to ``sys.stdout`` if ``None``.
diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h
index b6945eb1a..9852fc9d6 100644
--- a/src/Tk/_tkmini.h
+++ b/src/Tk/_tkmini.h
@@ -1,7 +1,7 @@
/* Small excerpts from the Tcl / Tk 8.6 headers
*
* License terms copied from:
- * http://www.tcl.tk/software/tcltk/license.html
+ * https://www.tcl.tk/software/tcltk/license.html
* as of 20 May 2016.
*
* Copyright (c) 1987-1994 The Regents of the University of California.
diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c
index 1c6c5f34a..9ae7edff1 100644
--- a/src/Tk/tkImaging.c
+++ b/src/Tk/tkImaging.c
@@ -219,7 +219,7 @@ TkImaging_Init(Tcl_Interp *interp) {
#define TKINTER_FINDER "PIL._tkinter_finder"
-#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32)
+#if defined(_WIN32) || defined(__WIN32__) || defined(WIN32) || defined(__CYGWIN__)
/*
* On Windows, we can't load the tkinter module to get the Tcl or Tk symbols,
diff --git a/src/_imaging.c b/src/_imaging.c
index a5b12d325..2a42c0461 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -498,7 +498,7 @@ getink(PyObject *color, Imaging im, char *ink) {
be cast to either UINT8 or INT32 */
int rIsInt = 0;
- if (PyTuple_Check(color) && PyTuple_Size(color) == 1) {
+ if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) {
color = PyTuple_GetItem(color, 0);
}
if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 ||
@@ -527,7 +527,10 @@ getink(PyObject *color, Imaging im, char *ink) {
if (im->bands == 1) {
/* unsigned integer, single layer */
if (rIsInt != 1) {
- if (!PyArg_ParseTuple(color, "L", &r)) {
+ if (PyTuple_GET_SIZE(color) != 1) {
+ PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple");
+ return NULL;
+ } else if (!PyArg_ParseTuple(color, "L", &r)) {
return NULL;
}
}
@@ -542,13 +545,20 @@ getink(PyObject *color, Imaging im, char *ink) {
g = (UINT8)(r >> 8);
r = (UINT8)r;
} else {
+ int tupleSize = PyTuple_GET_SIZE(color);
if (im->bands == 2) {
- if (!PyArg_ParseTuple(color, "L|i", &r, &a)) {
+ if (tupleSize != 1 && tupleSize != 2) {
+ PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements");
+ return NULL;
+ } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) {
return NULL;
}
g = b = r;
} else {
- if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) {
+ if (tupleSize != 3 && tupleSize != 4) {
+ PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one, three or four elements");
+ return NULL;
+ } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) {
return NULL;
}
}
@@ -1086,7 +1096,7 @@ _getpalette(ImagingObject *self, PyObject *args) {
}
static PyObject *
-_getpalettemode(ImagingObject *self, PyObject *args) {
+_getpalettemode(ImagingObject *self) {
if (!self->image->palette) {
PyErr_SetString(PyExc_ValueError, no_palette);
return NULL;
@@ -1109,7 +1119,12 @@ _getxy(PyObject *xy, int *x, int *y) {
} else if (PyFloat_Check(value)) {
*x = (int)PyFloat_AS_DOUBLE(value);
} else {
- goto badval;
+ PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL);
+ if (int_value != NULL && PyLong_Check(int_value)) {
+ *x = PyLong_AS_LONG(int_value);
+ } else {
+ goto badval;
+ }
}
value = PyTuple_GET_ITEM(xy, 1);
@@ -1118,7 +1133,12 @@ _getxy(PyObject *xy, int *x, int *y) {
} else if (PyFloat_Check(value)) {
*y = (int)PyFloat_AS_DOUBLE(value);
} else {
- goto badval;
+ PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL);
+ if (int_value != NULL && PyLong_Check(int_value)) {
+ *y = PyLong_AS_LONG(int_value);
+ } else {
+ goto badval;
+ }
}
return 0;
@@ -1474,6 +1494,14 @@ _putdata(ImagingObject *self, PyObject *args) {
return NULL;
}
+#define set_value_to_item(seq, i) \
+op = PySequence_Fast_GET_ITEM(seq, i); \
+if (PySequence_Check(op)) { \
+ PyErr_SetString(PyExc_TypeError, "sequence must be flattened"); \
+ return NULL; \
+} else { \
+ value = PyFloat_AsDouble(op); \
+}
if (image->image8) {
if (PyBytes_Check(data)) {
unsigned char *p;
@@ -1502,11 +1530,12 @@ _putdata(ImagingObject *self, PyObject *args) {
PyErr_SetString(PyExc_TypeError, must_be_sequence);
return NULL;
}
+ double value;
if (scale == 1.0 && offset == 0.0) {
/* Clipped data */
for (i = x = y = 0; i < n; i++) {
- op = PySequence_Fast_GET_ITEM(seq, i);
- image->image8[y][x] = (UINT8)CLIP8(PyLong_AsLong(op));
+ set_value_to_item(seq, i);
+ image->image8[y][x] = (UINT8)CLIP8(value);
if (++x >= (int)image->xsize) {
x = 0, y++;
}
@@ -1515,9 +1544,8 @@ _putdata(ImagingObject *self, PyObject *args) {
} else {
/* Scaled and clipped data */
for (i = x = y = 0; i < n; i++) {
- PyObject *op = PySequence_Fast_GET_ITEM(seq, i);
- image->image8[y][x] =
- CLIP8((int)(PyFloat_AsDouble(op) * scale + offset));
+ set_value_to_item(seq, i);
+ image->image8[y][x] = CLIP8(value * scale + offset);
if (++x >= (int)image->xsize) {
x = 0, y++;
}
@@ -1535,9 +1563,10 @@ _putdata(ImagingObject *self, PyObject *args) {
switch (image->type) {
case IMAGING_TYPE_INT32:
for (i = x = y = 0; i < n; i++) {
- op = PySequence_Fast_GET_ITEM(seq, i);
+ double value;
+ set_value_to_item(seq, i);
IMAGING_PIXEL_INT32(image, x, y) =
- (INT32)(PyFloat_AsDouble(op) * scale + offset);
+ (INT32)(value * scale + offset);
if (++x >= (int)image->xsize) {
x = 0, y++;
}
@@ -1546,9 +1575,10 @@ _putdata(ImagingObject *self, PyObject *args) {
break;
case IMAGING_TYPE_FLOAT32:
for (i = x = y = 0; i < n; i++) {
- op = PySequence_Fast_GET_ITEM(seq, i);
+ double value;
+ set_value_to_item(seq, i);
IMAGING_PIXEL_FLOAT32(image, x, y) =
- (FLOAT32)(PyFloat_AsDouble(op) * scale + offset);
+ (FLOAT32)(value * scale + offset);
if (++x >= (int)image->xsize) {
x = 0, y++;
}
@@ -1643,8 +1673,7 @@ _putpalette(ImagingObject *self, PyObject *args) {
unpack(self->image->palette->palette, palette, palettesize * 8 / bits);
- Py_INCREF(Py_None);
- return Py_None;
+ return PyLong_FromLong(palettesize * 8 / bits);
}
static PyObject *
@@ -2085,12 +2114,12 @@ _box_blur(ImagingObject *self, PyObject *args) {
/* -------------------------------------------------------------------- */
static PyObject *
-_isblock(ImagingObject *self, PyObject *args) {
+_isblock(ImagingObject *self) {
return PyBool_FromLong(self->image->block != NULL);
}
static PyObject *
-_getbbox(ImagingObject *self, PyObject *args) {
+_getbbox(ImagingObject *self) {
int bbox[4];
if (!ImagingGetBBox(self->image, bbox)) {
Py_INCREF(Py_None);
@@ -2135,7 +2164,7 @@ _getcolors(ImagingObject *self, PyObject *args) {
}
static PyObject *
-_getextrema(ImagingObject *self, PyObject *args) {
+_getextrema(ImagingObject *self) {
union {
UINT8 u[2];
INT32 i[2];
@@ -2169,7 +2198,7 @@ _getextrema(ImagingObject *self, PyObject *args) {
}
static PyObject *
-_getprojection(ImagingObject *self, PyObject *args) {
+_getprojection(ImagingObject *self) {
unsigned char *xprofile;
unsigned char *yprofile;
PyObject *result;
@@ -2287,7 +2316,7 @@ _merge(PyObject *self, PyObject *args) {
}
static PyObject *
-_split(ImagingObject *self, PyObject *args) {
+_split(ImagingObject *self) {
int fails = 0;
Py_ssize_t i;
PyObject *list;
@@ -2318,7 +2347,7 @@ _split(ImagingObject *self, PyObject *args) {
#ifdef WITH_IMAGECHOPS
static PyObject *
-_chop_invert(ImagingObject *self, PyObject *args) {
+_chop_invert(ImagingObject *self) {
return PyImagingNew(ImagingNegative(self->image));
}
@@ -2697,8 +2726,8 @@ _font_getsize(ImagingFontObject *self, PyObject *args) {
}
static struct PyMethodDef _font_methods[] = {
- {"getmask", (PyCFunction)_font_getmask, 1},
- {"getsize", (PyCFunction)_font_getsize, 1},
+ {"getmask", (PyCFunction)_font_getmask, METH_VARARGS},
+ {"getsize", (PyCFunction)_font_getsize, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
@@ -3105,7 +3134,8 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
PyObject *data;
int ink;
int fill = 0;
- if (!PyArg_ParseTuple(args, "Oi|i", &data, &ink, &fill)) {
+ int width = 0;
+ if (!PyArg_ParseTuple(args, "Oi|ii", &data, &ink, &fill, &width)) {
return NULL;
}
@@ -3134,7 +3164,7 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) {
free(xy);
- if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, self->blend) < 0) {
+ if (ImagingDrawPolygon(self->image->image, n, ixy, &ink, fill, width, self->blend) < 0) {
free(ixy);
return NULL;
}
@@ -3192,19 +3222,19 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) {
static struct PyMethodDef _draw_methods[] = {
#ifdef WITH_IMAGEDRAW
/* Graphics (ImageDraw) */
- {"draw_lines", (PyCFunction)_draw_lines, 1},
+ {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS},
#ifdef WITH_ARROW
- {"draw_outline", (PyCFunction)_draw_outline, 1},
+ {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS},
#endif
- {"draw_polygon", (PyCFunction)_draw_polygon, 1},
- {"draw_rectangle", (PyCFunction)_draw_rectangle, 1},
- {"draw_points", (PyCFunction)_draw_points, 1},
- {"draw_arc", (PyCFunction)_draw_arc, 1},
- {"draw_bitmap", (PyCFunction)_draw_bitmap, 1},
- {"draw_chord", (PyCFunction)_draw_chord, 1},
- {"draw_ellipse", (PyCFunction)_draw_ellipse, 1},
- {"draw_pieslice", (PyCFunction)_draw_pieslice, 1},
- {"draw_ink", (PyCFunction)_draw_ink, 1},
+ {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS},
+ {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS},
+ {"draw_points", (PyCFunction)_draw_points, METH_VARARGS},
+ {"draw_arc", (PyCFunction)_draw_arc, METH_VARARGS},
+ {"draw_bitmap", (PyCFunction)_draw_bitmap, METH_VARARGS},
+ {"draw_chord", (PyCFunction)_draw_chord, METH_VARARGS},
+ {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS},
+ {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS},
+ {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS},
#endif
{NULL, NULL} /* sentinel */
};
@@ -3411,100 +3441,100 @@ _save_ppm(ImagingObject *self, PyObject *args) {
static struct PyMethodDef methods[] = {
/* Put commonly used methods first */
- {"getpixel", (PyCFunction)_getpixel, 1},
- {"putpixel", (PyCFunction)_putpixel, 1},
+ {"getpixel", (PyCFunction)_getpixel, METH_VARARGS},
+ {"putpixel", (PyCFunction)_putpixel, METH_VARARGS},
- {"pixel_access", (PyCFunction)pixel_access_new, 1},
+ {"pixel_access", (PyCFunction)pixel_access_new, METH_VARARGS},
/* Standard processing methods (Image) */
- {"color_lut_3d", (PyCFunction)_color_lut_3d, 1},
- {"convert", (PyCFunction)_convert, 1},
- {"convert2", (PyCFunction)_convert2, 1},
- {"convert_matrix", (PyCFunction)_convert_matrix, 1},
- {"convert_transparent", (PyCFunction)_convert_transparent, 1},
- {"copy", (PyCFunction)_copy, 1},
- {"crop", (PyCFunction)_crop, 1},
- {"expand", (PyCFunction)_expand_image, 1},
- {"filter", (PyCFunction)_filter, 1},
- {"histogram", (PyCFunction)_histogram, 1},
- {"entropy", (PyCFunction)_entropy, 1},
+ {"color_lut_3d", (PyCFunction)_color_lut_3d, METH_VARARGS},
+ {"convert", (PyCFunction)_convert, METH_VARARGS},
+ {"convert2", (PyCFunction)_convert2, METH_VARARGS},
+ {"convert_matrix", (PyCFunction)_convert_matrix, METH_VARARGS},
+ {"convert_transparent", (PyCFunction)_convert_transparent, METH_VARARGS},
+ {"copy", (PyCFunction)_copy, METH_VARARGS},
+ {"crop", (PyCFunction)_crop, METH_VARARGS},
+ {"expand", (PyCFunction)_expand_image, METH_VARARGS},
+ {"filter", (PyCFunction)_filter, METH_VARARGS},
+ {"histogram", (PyCFunction)_histogram, METH_VARARGS},
+ {"entropy", (PyCFunction)_entropy, METH_VARARGS},
#ifdef WITH_MODEFILTER
- {"modefilter", (PyCFunction)_modefilter, 1},
+ {"modefilter", (PyCFunction)_modefilter, METH_VARARGS},
#endif
- {"offset", (PyCFunction)_offset, 1},
- {"paste", (PyCFunction)_paste, 1},
- {"point", (PyCFunction)_point, 1},
- {"point_transform", (PyCFunction)_point_transform, 1},
- {"putdata", (PyCFunction)_putdata, 1},
+ {"offset", (PyCFunction)_offset, METH_VARARGS},
+ {"paste", (PyCFunction)_paste, METH_VARARGS},
+ {"point", (PyCFunction)_point, METH_VARARGS},
+ {"point_transform", (PyCFunction)_point_transform, METH_VARARGS},
+ {"putdata", (PyCFunction)_putdata, METH_VARARGS},
#ifdef WITH_QUANTIZE
- {"quantize", (PyCFunction)_quantize, 1},
+ {"quantize", (PyCFunction)_quantize, METH_VARARGS},
#endif
#ifdef WITH_RANKFILTER
- {"rankfilter", (PyCFunction)_rankfilter, 1},
+ {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS},
#endif
- {"resize", (PyCFunction)_resize, 1},
- {"reduce", (PyCFunction)_reduce, 1},
- {"transpose", (PyCFunction)_transpose, 1},
- {"transform2", (PyCFunction)_transform2, 1},
+ {"resize", (PyCFunction)_resize, METH_VARARGS},
+ {"reduce", (PyCFunction)_reduce, METH_VARARGS},
+ {"transpose", (PyCFunction)_transpose, METH_VARARGS},
+ {"transform2", (PyCFunction)_transform2, METH_VARARGS},
- {"isblock", (PyCFunction)_isblock, 1},
+ {"isblock", (PyCFunction)_isblock, METH_NOARGS},
- {"getbbox", (PyCFunction)_getbbox, 1},
- {"getcolors", (PyCFunction)_getcolors, 1},
- {"getextrema", (PyCFunction)_getextrema, 1},
- {"getprojection", (PyCFunction)_getprojection, 1},
+ {"getbbox", (PyCFunction)_getbbox, METH_NOARGS},
+ {"getcolors", (PyCFunction)_getcolors, METH_VARARGS},
+ {"getextrema", (PyCFunction)_getextrema, METH_NOARGS},
+ {"getprojection", (PyCFunction)_getprojection, METH_NOARGS},
- {"getband", (PyCFunction)_getband, 1},
- {"putband", (PyCFunction)_putband, 1},
- {"split", (PyCFunction)_split, 1},
- {"fillband", (PyCFunction)_fillband, 1},
+ {"getband", (PyCFunction)_getband, METH_VARARGS},
+ {"putband", (PyCFunction)_putband, METH_VARARGS},
+ {"split", (PyCFunction)_split, METH_NOARGS},
+ {"fillband", (PyCFunction)_fillband, METH_VARARGS},
- {"setmode", (PyCFunction)im_setmode, 1},
+ {"setmode", (PyCFunction)im_setmode, METH_VARARGS},
- {"getpalette", (PyCFunction)_getpalette, 1},
- {"getpalettemode", (PyCFunction)_getpalettemode, 1},
- {"putpalette", (PyCFunction)_putpalette, 1},
- {"putpalettealpha", (PyCFunction)_putpalettealpha, 1},
- {"putpalettealphas", (PyCFunction)_putpalettealphas, 1},
+ {"getpalette", (PyCFunction)_getpalette, METH_VARARGS},
+ {"getpalettemode", (PyCFunction)_getpalettemode, METH_NOARGS},
+ {"putpalette", (PyCFunction)_putpalette, METH_VARARGS},
+ {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS},
+ {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS},
#ifdef WITH_IMAGECHOPS
/* Channel operations (ImageChops) */
- {"chop_invert", (PyCFunction)_chop_invert, 1},
- {"chop_lighter", (PyCFunction)_chop_lighter, 1},
- {"chop_darker", (PyCFunction)_chop_darker, 1},
- {"chop_difference", (PyCFunction)_chop_difference, 1},
- {"chop_multiply", (PyCFunction)_chop_multiply, 1},
- {"chop_screen", (PyCFunction)_chop_screen, 1},
- {"chop_add", (PyCFunction)_chop_add, 1},
- {"chop_subtract", (PyCFunction)_chop_subtract, 1},
- {"chop_add_modulo", (PyCFunction)_chop_add_modulo, 1},
- {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, 1},
- {"chop_and", (PyCFunction)_chop_and, 1},
- {"chop_or", (PyCFunction)_chop_or, 1},
- {"chop_xor", (PyCFunction)_chop_xor, 1},
- {"chop_soft_light", (PyCFunction)_chop_soft_light, 1},
- {"chop_hard_light", (PyCFunction)_chop_hard_light, 1},
- {"chop_overlay", (PyCFunction)_chop_overlay, 1},
+ {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS},
+ {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS},
+ {"chop_darker", (PyCFunction)_chop_darker, METH_VARARGS},
+ {"chop_difference", (PyCFunction)_chop_difference, METH_VARARGS},
+ {"chop_multiply", (PyCFunction)_chop_multiply, METH_VARARGS},
+ {"chop_screen", (PyCFunction)_chop_screen, METH_VARARGS},
+ {"chop_add", (PyCFunction)_chop_add, METH_VARARGS},
+ {"chop_subtract", (PyCFunction)_chop_subtract, METH_VARARGS},
+ {"chop_add_modulo", (PyCFunction)_chop_add_modulo, METH_VARARGS},
+ {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, METH_VARARGS},
+ {"chop_and", (PyCFunction)_chop_and, METH_VARARGS},
+ {"chop_or", (PyCFunction)_chop_or, METH_VARARGS},
+ {"chop_xor", (PyCFunction)_chop_xor, METH_VARARGS},
+ {"chop_soft_light", (PyCFunction)_chop_soft_light, METH_VARARGS},
+ {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS},
+ {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS},
#endif
#ifdef WITH_UNSHARPMASK
/* Kevin Cazabon's unsharpmask extension */
- {"gaussian_blur", (PyCFunction)_gaussian_blur, 1},
- {"unsharp_mask", (PyCFunction)_unsharp_mask, 1},
+ {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS},
+ {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS},
#endif
- {"box_blur", (PyCFunction)_box_blur, 1},
+ {"box_blur", (PyCFunction)_box_blur, METH_VARARGS},
#ifdef WITH_EFFECTS
/* Special effects */
- {"effect_spread", (PyCFunction)_effect_spread, 1},
+ {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS},
#endif
/* Misc. */
- {"new_block", (PyCFunction)_new_block, 1},
+ {"new_block", (PyCFunction)_new_block, METH_VARARGS},
- {"save_ppm", (PyCFunction)_save_ppm, 1},
+ {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
@@ -3979,111 +4009,111 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args);
static PyMethodDef functions[] = {
/* Object factories */
- {"alpha_composite", (PyCFunction)_alpha_composite, 1},
- {"blend", (PyCFunction)_blend, 1},
- {"fill", (PyCFunction)_fill, 1},
- {"new", (PyCFunction)_new, 1},
- {"merge", (PyCFunction)_merge, 1},
+ {"alpha_composite", (PyCFunction)_alpha_composite, METH_VARARGS},
+ {"blend", (PyCFunction)_blend, METH_VARARGS},
+ {"fill", (PyCFunction)_fill, METH_VARARGS},
+ {"new", (PyCFunction)_new, METH_VARARGS},
+ {"merge", (PyCFunction)_merge, METH_VARARGS},
/* Functions */
- {"convert", (PyCFunction)_convert2, 1},
+ {"convert", (PyCFunction)_convert2, METH_VARARGS},
/* Codecs */
- {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, 1},
- {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, 1},
- {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1},
- {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, 1},
- {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, 1},
- {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, 1},
- {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, 1},
- {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, /* EPS=HEX! */
+ {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS},
+ {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS},
+ {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS},
+ {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS},
+ {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, METH_VARARGS},
+ {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, METH_VARARGS},
+ {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, METH_VARARGS},
+ {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, /* EPS=HEX! */
#ifdef HAVE_LIBJPEG
- {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, 1},
- {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, 1},
+ {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, METH_VARARGS},
+ {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, METH_VARARGS},
#endif
#ifdef HAVE_OPENJPEG
- {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, 1},
- {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, 1},
+ {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, METH_VARARGS},
+ {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, METH_VARARGS},
#endif
#ifdef HAVE_LIBTIFF
- {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1},
- {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1},
+ {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, METH_VARARGS},
+ {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, METH_VARARGS},
#endif
- {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, 1},
- {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, 1},
- {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, 1},
- {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, 1},
- {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, 1},
- {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, 1},
- {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1},
- {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1},
- {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1},
- {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, 1},
- {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1},
- {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1},
+ {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, METH_VARARGS},
+ {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, METH_VARARGS},
+ {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, METH_VARARGS},
+ {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, METH_VARARGS},
+ {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, METH_VARARGS},
+ {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, METH_VARARGS},
+ {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, METH_VARARGS},
+ {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, METH_VARARGS},
+ {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, METH_VARARGS},
+ {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, METH_VARARGS},
+ {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, METH_VARARGS},
+ {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, METH_VARARGS},
#ifdef HAVE_LIBZ
- {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, 1},
- {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, 1},
+ {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, METH_VARARGS},
+ {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS},
#endif
/* Memory mapping */
#ifdef WITH_MAPPING
- {"map_buffer", (PyCFunction)PyImaging_MapBuffer, 1},
+ {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS},
#endif
/* Display support */
#ifdef _WIN32
- {"display", (PyCFunction)PyImaging_DisplayWin32, 1},
- {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, 1},
- {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, 1},
- {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, 1},
- {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, 1},
- {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, 1},
- {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, 1},
- {"drawwmf", (PyCFunction)PyImaging_DrawWmf, 1},
+ {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS},
+ {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS},
+ {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS},
+ {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS},
+ {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS},
+ {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS},
+ {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS},
+ {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS},
#endif
#ifdef HAVE_XCB
- {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, 1},
+ {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, METH_VARARGS},
#endif
/* Utilities */
- {"getcodecstatus", (PyCFunction)_getcodecstatus, 1},
+ {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS},
/* Special effects (experimental) */
#ifdef WITH_EFFECTS
- {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, 1},
- {"effect_noise", (PyCFunction)_effect_noise, 1},
- {"linear_gradient", (PyCFunction)_linear_gradient, 1},
- {"radial_gradient", (PyCFunction)_radial_gradient, 1},
- {"wedge", (PyCFunction)_linear_gradient, 1}, /* Compatibility */
+ {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS},
+ {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS},
+ {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS},
+ {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS},
+ {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */
#endif
/* Drawing support stuff */
#ifdef WITH_IMAGEDRAW
- {"font", (PyCFunction)_font_new, 1},
- {"draw", (PyCFunction)_draw_new, 1},
+ {"font", (PyCFunction)_font_new, METH_VARARGS},
+ {"draw", (PyCFunction)_draw_new, METH_VARARGS},
#endif
/* Experimental path stuff */
#ifdef WITH_IMAGEPATH
- {"path", (PyCFunction)PyPath_Create, 1},
+ {"path", (PyCFunction)PyPath_Create, METH_VARARGS},
#endif
/* Experimental arrow graphics stuff */
#ifdef WITH_ARROW
- {"outline", (PyCFunction)PyOutline_Create, 1},
+ {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS},
#endif
/* Resource management */
- {"get_stats", (PyCFunction)_get_stats, 1},
- {"reset_stats", (PyCFunction)_reset_stats, 1},
- {"get_alignment", (PyCFunction)_get_alignment, 1},
- {"get_block_size", (PyCFunction)_get_block_size, 1},
- {"get_blocks_max", (PyCFunction)_get_blocks_max, 1},
- {"set_alignment", (PyCFunction)_set_alignment, 1},
- {"set_block_size", (PyCFunction)_set_block_size, 1},
- {"set_blocks_max", (PyCFunction)_set_blocks_max, 1},
- {"clear_cache", (PyCFunction)_clear_cache, 1},
+ {"get_stats", (PyCFunction)_get_stats, METH_VARARGS},
+ {"reset_stats", (PyCFunction)_reset_stats, METH_VARARGS},
+ {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS},
+ {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS},
+ {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS},
+ {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS},
+ {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS},
+ {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS},
+ {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
diff --git a/src/_imagingcms.c b/src/_imagingcms.c
index 314150420..9b5a121d7 100644
--- a/src/_imagingcms.c
+++ b/src/_imagingcms.c
@@ -3,14 +3,14 @@
* a Python / PIL interface to the littleCMS ICC Color Management System
* Copyright (C) 2002-2003 Kevin Cazabon
* kevin@cazabon.com
- * http://www.cazabon.com
+ * https://www.cazabon.com
* Adapted/reworked for PIL by Fredrik Lundh
* Copyright (c) 2009 Fredrik Lundh
* Updated to LCMS2
* Copyright (c) 2013 Eric Soroos
*
- * pyCMS home page: http://www.cazabon.com/pyCMS
- * littleCMS home page: http://www.littlecms.com
+ * pyCMS home page: https://www.cazabon.com/pyCMS
+ * littleCMS home page: https://www.littlecms.com
* (littleCMS is Copyright (C) 1998-2001 Marti Maria)
*
* Originally released under LGPL. Graciously donated to PIL in
@@ -23,7 +23,7 @@ pyCMS\n\
a Python / PIL interface to the littleCMS ICC Color Management System\n\
Copyright (C) 2002-2003 Kevin Cazabon\n\
kevin@cazabon.com\n\
-http://www.cazabon.com\n\
+https://www.cazabon.com\n\
"
#define PY_SSIZE_T_CLEAN
@@ -959,25 +959,25 @@ _is_intent_supported(CmsProfileObject *self, int clut) {
static PyMethodDef pyCMSdll_methods[] = {
- {"profile_open", cms_profile_open, 1},
- {"profile_frombytes", cms_profile_fromstring, 1},
- {"profile_fromstring", cms_profile_fromstring, 1},
- {"profile_tobytes", cms_profile_tobytes, 1},
+ {"profile_open", cms_profile_open, METH_VARARGS},
+ {"profile_frombytes", cms_profile_fromstring, METH_VARARGS},
+ {"profile_fromstring", cms_profile_fromstring, METH_VARARGS},
+ {"profile_tobytes", cms_profile_tobytes, METH_VARARGS},
/* profile and transform functions */
- {"buildTransform", buildTransform, 1},
- {"buildProofTransform", buildProofTransform, 1},
- {"createProfile", createProfile, 1},
+ {"buildTransform", buildTransform, METH_VARARGS},
+ {"buildProofTransform", buildProofTransform, METH_VARARGS},
+ {"createProfile", createProfile, METH_VARARGS},
/* platform specific tools */
#ifdef _WIN32
- {"get_display_profile_win32", cms_get_display_profile_win32, 1},
+ {"get_display_profile_win32", cms_get_display_profile_win32, METH_VARARGS},
#endif
{NULL, NULL}};
static struct PyMethodDef cms_profile_methods[] = {
- {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, 1},
+ {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 73f0f6362..8f19b763c 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -277,7 +277,8 @@ text_layout_raqm(
direction = RAQM_DIRECTION_LTR;
} else if (strcmp(dir, "ttb") == 0) {
direction = RAQM_DIRECTION_TTB;
-#if !defined(RAQM_VERSION_ATLEAST) || !RAQM_VERSION_ATLEAST(0, 7, 0)
+#if !defined(RAQM_VERSION_ATLEAST)
+ /* RAQM_VERSION_ATLEAST was added in Raqm 0.7.0 */
PyErr_SetString(
PyExc_ValueError,
"libraqm 0.7 or greater required for 'ttb' direction");
@@ -832,7 +833,7 @@ font_render(FontObject *self, PyObject *args) {
}
im = (Imaging)id;
- load_flags = FT_LOAD_DEFAULT;
+ load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT;
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
@@ -932,11 +933,7 @@ font_render(FontObject *self, PyObject *args) {
case FT_PIXEL_MODE_GRAY2:
case FT_PIXEL_MODE_GRAY4:
if (!bitmap_converted_ready) {
-#if FREETYPE_MAJOR > 2 || (FREETYPE_MAJOR == 2 && FREETYPE_MINOR > 6)
FT_Bitmap_Init(&bitmap_converted);
-#else
- FT_Bitmap_New(&bitmap_converted);
-#endif
bitmap_converted_ready = 1;
}
error = FT_Bitmap_Convert(library, &bitmap, &bitmap_converted, 1);
diff --git a/src/_webp.c b/src/_webp.c
index 4d51d99df..fd99116cb 100644
--- a/src/_webp.c
+++ b/src/_webp.c
@@ -392,10 +392,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) {
return (PyObject *)decp;
}
}
+ WebPDataClear(&(decp->data));
}
PyObject_Del(decp);
}
- PyErr_SetString(PyExc_RuntimeError, "could not create decoder object");
+ PyErr_SetString(PyExc_OSError, "could not create decoder object");
return NULL;
}
@@ -485,7 +486,7 @@ static struct PyMethodDef _anim_encoder_methods[] = {
{NULL, NULL} /* sentinel */
};
-// WebPAnimDecoder type definition
+// WebPAnimEncoder type definition
static PyTypeObject WebPAnimEncoder_Type = {
PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */
sizeof(WebPAnimEncoderObject), /*tp_size */
@@ -663,7 +664,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
WebPPictureFree(&pic);
if (!ok) {
- PyErr_SetString(PyExc_ValueError, "encoding error");
+ PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code);
return NULL;
}
output = writer.mem;
diff --git a/src/decode.c b/src/decode.c
index 5d64bd0b9..e236264cd 100644
--- a/src/decode.c
+++ b/src/decode.c
@@ -34,9 +34,10 @@
#include "libImaging/Imaging.h"
+#include "libImaging/Bit.h"
+#include "libImaging/Bcn.h"
#include "libImaging/Gif.h"
#include "libImaging/Raw.h"
-#include "libImaging/Bit.h"
#include "libImaging/Sgi.h"
/* -------------------------------------------------------------------- */
@@ -199,7 +200,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) {
state->bytes = (state->bits * state->xsize + 7) / 8;
}
/* malloc check ok, overflow checked above */
- state->buffer = (UINT8 *)malloc(state->bytes);
+ state->buffer = (UINT8 *)calloc(1, state->bytes);
if (!state->buffer) {
return ImagingError_MemoryError();
}
@@ -234,15 +235,15 @@ _setfd(ImagingDecoderObject *decoder, PyObject *args) {
}
static PyObject *
-_get_pulls_fd(ImagingDecoderObject *decoder) {
+_get_pulls_fd(ImagingDecoderObject *decoder, void *closure) {
return PyBool_FromLong(decoder->pulls_fd);
}
static struct PyMethodDef methods[] = {
- {"decode", (PyCFunction)_decode, 1},
- {"cleanup", (PyCFunction)_decode_cleanup, 1},
- {"setimage", (PyCFunction)_setimage, 1},
- {"setfd", (PyCFunction)_setfd, 1},
+ {"decode", (PyCFunction)_decode, METH_VARARGS},
+ {"cleanup", (PyCFunction)_decode_cleanup, METH_VARARGS},
+ {"setimage", (PyCFunction)_setimage, METH_VARARGS},
+ {"setfd", (PyCFunction)_setfd, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
@@ -298,7 +299,7 @@ get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmod
unpack = ImagingFindUnpacker(mode, rawmode, &bits);
if (!unpack) {
Py_DECREF(decoder);
- PyErr_SetString(PyExc_ValueError, "unknown raw mode");
+ PyErr_SetString(PyExc_ValueError, "unknown raw mode for given image mode");
return -1;
}
@@ -359,8 +360,8 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) {
char *mode;
char *actual;
int n = 0;
- int ystep = 1;
- if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep)) {
+ char *pixel_format = "";
+ if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) {
return NULL;
}
@@ -368,13 +369,15 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) {
case 1: /* BC1: 565 color, 1-bit alpha */
case 2: /* BC2: 565 color, 4-bit alpha */
case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */
- case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */
case 7: /* BC7: 4-channel 8-bit via everything */
actual = "RGBA";
break;
case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */
actual = "L";
break;
+ case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */
+ actual = "RGB";
+ break;
case 6: /* BC6: 3-channel 16-bit float */
/* TODO: support 4-channel floating point images */
actual = "RGBAF";
@@ -389,14 +392,14 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) {
return NULL;
}
- decoder = PyImaging_DecoderNew(0);
+ decoder = PyImaging_DecoderNew(sizeof(char *));
if (decoder == NULL) {
return NULL;
}
decoder->decode = ImagingBcnDecode;
decoder->state.state = n;
- decoder->state.ystep = ystep;
+ ((BCNSTATE *)decoder->state.context)->pixel_format = pixel_format;
return (PyObject *)decoder;
}
@@ -497,7 +500,7 @@ PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) {
char *rawmode;
char *compname;
int fp;
- uint32 ifdoffset;
+ uint32_t ifdoffset;
if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) {
return NULL;
diff --git a/src/display.c b/src/display.c
index 3541655cf..0ce10e249 100644
--- a/src/display.c
+++ b/src/display.c
@@ -224,14 +224,14 @@ _tobytes(ImagingDisplayObject *display, PyObject *args) {
}
static struct PyMethodDef methods[] = {
- {"draw", (PyCFunction)_draw, 1},
- {"expose", (PyCFunction)_expose, 1},
- {"paste", (PyCFunction)_paste, 1},
- {"query_palette", (PyCFunction)_query_palette, 1},
- {"getdc", (PyCFunction)_getdc, 1},
- {"releasedc", (PyCFunction)_releasedc, 1},
- {"frombytes", (PyCFunction)_frombytes, 1},
- {"tobytes", (PyCFunction)_tobytes, 1},
+ {"draw", (PyCFunction)_draw, METH_VARARGS},
+ {"expose", (PyCFunction)_expose, METH_VARARGS},
+ {"paste", (PyCFunction)_paste, METH_VARARGS},
+ {"query_palette", (PyCFunction)_query_palette, METH_VARARGS},
+ {"getdc", (PyCFunction)_getdc, METH_VARARGS},
+ {"releasedc", (PyCFunction)_releasedc, METH_VARARGS},
+ {"frombytes", (PyCFunction)_frombytes, METH_VARARGS},
+ {"tobytes", (PyCFunction)_tobytes, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
diff --git a/src/encode.c b/src/encode.c
index f92ba62c2..2ecf9723b 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -264,7 +264,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) {
}
state->bytes = (state->bits * state->xsize + 7) / 8;
/* malloc check ok, overflow checked above */
- state->buffer = (UINT8 *)malloc(state->bytes);
+ state->buffer = (UINT8 *)calloc(1, state->bytes);
if (!state->buffer) {
return ImagingError_MemoryError();
}
@@ -299,17 +299,17 @@ _setfd(ImagingEncoderObject *encoder, PyObject *args) {
}
static PyObject *
-_get_pushes_fd(ImagingEncoderObject *encoder) {
+_get_pushes_fd(ImagingEncoderObject *encoder, void *closure) {
return PyBool_FromLong(encoder->pushes_fd);
}
static struct PyMethodDef methods[] = {
- {"encode", (PyCFunction)_encode, 1},
- {"cleanup", (PyCFunction)_encode_cleanup, 1},
- {"encode_to_file", (PyCFunction)_encode_to_file, 1},
- {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, 1},
- {"setimage", (PyCFunction)_setimage, 1},
- {"setfd", (PyCFunction)_setfd, 1},
+ {"encode", (PyCFunction)_encode, METH_VARARGS},
+ {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS},
+ {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS},
+ {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_VARARGS},
+ {"setimage", (PyCFunction)_setimage, METH_VARARGS},
+ {"setfd", (PyCFunction)_setfd, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
@@ -644,10 +644,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
int key_int, status, is_core_tag, is_var_length, num_core_tags, i;
TIFFDataType type = TIFF_NOTYPE;
// This list also exists in TiffTags.py
- const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274,
- 277, 278, 280, 281, 340, 341, 282, 283, 284,
- 286, 287, 296, 297, 320, 321, 338, 32995, 32998,
- 32996, 339, 32997, 330, 531, 530, 65537};
+ const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274,
+ 277, 278, 280, 281, 340, 341, 282, 283, 284,
+ 286, 287, 296, 297, 320, 321, 338, 32995, 32998,
+ 32996, 339, 32997, 330, 531, 530, 65537, 301, 532};
Py_ssize_t tags_size;
PyObject *item;
@@ -790,7 +790,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
int stride = 256;
if (len != 768) {
PyErr_SetString(
- PyExc_ValueError, "Requiring 768 items for for Colormap");
+ PyExc_ValueError, "Requiring 768 items for Colormap");
return NULL;
}
UINT16 *av;
@@ -808,6 +808,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
av + stride * 2);
free(av);
}
+ } else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) {
+ status = ImagingLibTiffSetField(
+ &encoder->state,
+ (ttag_t)key_int,
+ (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)),
+ (UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1)));
} else if (type == TIFF_SHORT) {
UINT16 *av;
/* malloc check ok, calloc checks for overflow */
diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c
index 6bb16fe3a..514fb2929 100644
--- a/src/libImaging/Access.c
+++ b/src/libImaging/Access.c
@@ -11,7 +11,7 @@
#include "Imaging.h"
-/* use Tests/make_hash.py 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_HASH 3078
diff --git a/src/libImaging/Bcn.h b/src/libImaging/Bcn.h
new file mode 100644
index 000000000..1a6fbee45
--- /dev/null
+++ b/src/libImaging/Bcn.h
@@ -0,0 +1,3 @@
+typedef struct {
+ char *pixel_format;
+} BCNSTATE;
diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c
index b6a4cbadc..22b36eb7a 100644
--- a/src/libImaging/BcnDecode.c
+++ b/src/libImaging/BcnDecode.c
@@ -13,6 +13,8 @@
#include "Imaging.h"
+#include "Bcn.h"
+
typedef struct {
UINT8 r, g, b, a;
} rgba;
@@ -35,6 +37,11 @@ typedef struct {
UINT8 lut[6];
} bc3_alpha;
+typedef struct {
+ INT8 a0, a1;
+ UINT8 lut[6];
+} bc5s_alpha;
+
#define LOAD16(p) (p)[0] | ((p)[1] << 8)
#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24)
@@ -46,11 +53,6 @@ bc1_color_load(bc1_color *dst, const UINT8 *src) {
dst->lut = LOAD32(src + 4);
}
-static void
-bc3_alpha_load(bc3_alpha *dst, const UINT8 *src) {
- memcpy(dst, src, sizeof(bc3_alpha));
-}
-
static rgba
decode_565(UINT16 x) {
rgba c;
@@ -69,7 +71,7 @@ decode_565(UINT16 x) {
}
static void
-decode_bc1_color(rgba *dst, const UINT8 *src) {
+decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) {
bc1_color col;
rgba p[4];
int n, cw;
@@ -84,7 +86,10 @@ decode_bc1_color(rgba *dst, const UINT8 *src) {
r1 = p[1].r;
g1 = p[1].g;
b1 = p[1].b;
- if (col.c0 > col.c1) {
+
+
+ /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */
+ if (col.c0 > col.c1 || separate_alpha) {
p[2].r = (2 * r0 + 1 * r1) / 3;
p[2].g = (2 * g0 + 1 * g1) / 3;
p[2].b = (2 * b0 + 1 * b1) / 3;
@@ -110,15 +115,26 @@ decode_bc1_color(rgba *dst, const UINT8 *src) {
}
static void
-decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) {
- bc3_alpha b;
+decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) {
UINT16 a0, a1;
UINT8 a[8];
- int n, lut, aw;
- bc3_alpha_load(&b, src);
+ int n, lut1, lut2, aw;
+ if (sign == 1) {
+ bc5s_alpha b;
+ memcpy(&b, src, sizeof(bc5s_alpha));
+ a0 = (b.a0 + 255) / 2;
+ a1 = (b.a1 + 255) / 2;
+ lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16);
+ lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16);
+ } else {
+ bc3_alpha b;
+ memcpy(&b, src, sizeof(bc3_alpha));
+ a0 = b.a0;
+ a1 = b.a1;
+ lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16);
+ lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16);
+ }
- a0 = b.a0;
- a1 = b.a1;
a[0] = (UINT8)a0;
a[1] = (UINT8)a1;
if (a0 > a1) {
@@ -136,27 +152,25 @@ decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) {
a[6] = 0;
a[7] = 0xff;
}
- lut = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16);
for (n = 0; n < 8; n++) {
- aw = 7 & (lut >> (3 * n));
+ aw = 7 & (lut1 >> (3 * n));
dst[stride * n + o] = a[aw];
}
- lut = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16);
for (n = 0; n < 8; n++) {
- aw = 7 & (lut >> (3 * n));
+ aw = 7 & (lut2 >> (3 * n));
dst[stride * (8 + n) + o] = a[aw];
}
}
static void
decode_bc1_block(rgba *col, const UINT8 *src) {
- decode_bc1_color(col, src);
+ decode_bc1_color(col, src, 0);
}
static void
decode_bc2_block(rgba *col, const UINT8 *src) {
int n, bitI, byI, av;
- decode_bc1_color(col, src + 8);
+ decode_bc1_color(col, src + 8, 1);
for (n = 0; n < 16; n++) {
bitI = n * 4;
byI = bitI >> 3;
@@ -168,19 +182,19 @@ decode_bc2_block(rgba *col, const UINT8 *src) {
static void
decode_bc3_block(rgba *col, const UINT8 *src) {
- decode_bc1_color(col, src + 8);
- decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3);
+ decode_bc1_color(col, src + 8, 1);
+ decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3, 0);
}
static void
decode_bc4_block(lum *col, const UINT8 *src) {
- decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0);
+ decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, 0);
}
static void
-decode_bc5_block(rgba *col, const UINT8 *src) {
- decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0);
- decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1);
+decode_bc5_block(rgba *col, const UINT8 *src, int sign) {
+ decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, sign);
+ decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1, sign);
}
/* BC6 and BC7 are described here:
@@ -810,7 +824,7 @@ put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) {
static int
decode_bcn(
- Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C) {
+ Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C, char *pixel_format) {
int ymax = state->ysize + state->yoff;
const UINT8 *ptr = src;
switch (N) {
@@ -833,7 +847,19 @@ decode_bcn(
DECODE_LOOP(2, 16, rgba);
DECODE_LOOP(3, 16, rgba);
DECODE_LOOP(4, 8, lum);
- DECODE_LOOP(5, 16, rgba);
+ case 5:
+ while (bytes >= 16) {
+ rgba col[16];
+ memset(col, 0, 16 * sizeof(col[0]));
+ decode_bc5_block(col, ptr, strcmp(pixel_format, "BC5S") == 0 ? 1 : 0);
+ put_block(im, state, (const char *)col, sizeof(col[0]), C);
+ ptr += 16;
+ bytes -= 16;
+ if (state->y >= ymax) {
+ return -1;
+ }
+ }
+ break;
case 6:
while (bytes >= 16) {
rgb32f col[16];
@@ -846,7 +872,7 @@ decode_bcn(
}
}
break;
- DECODE_LOOP(7, 16, rgba);
+ DECODE_LOOP(7, 16, rgba);
#undef DECODE_LOOP
}
return (int)(ptr - src);
@@ -857,9 +883,7 @@ ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
int N = state->state & 0xf;
int width = state->xsize;
int height = state->ysize;
- if ((width & 3) | (height & 3)) {
- return decode_bcn(im, state, buf, bytes, N, 1);
- } else {
- return decode_bcn(im, state, buf, bytes, N, 0);
- }
+ int C = (width & 3) | (height & 3) ? 1 : 0;
+ char *pixel_format = ((BCNSTATE *)state->context)->pixel_format;
+ return decode_bcn(im, state, buf, bytes, N, C, pixel_format);
}
diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c
index 88862eb73..2e45a3358 100644
--- a/src/libImaging/BoxBlur.c
+++ b/src/libImaging/BoxBlur.c
@@ -287,7 +287,7 @@ ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) {
float sigma2, L, l, a;
sigma2 = radius * radius / passes;
- // from http://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf
+ // from https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf
// [7] Box length.
L = sqrt(12.0 * sigma2 + 1.0);
// [11] Integer part of box radius.
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index 8c7be36a2..517a4dbe3 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -1013,7 +1013,7 @@ p2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
int x;
/* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++) {
- *out++ = L(&palette[in[x] * 4]) / 1000;
+ *out++ = L24(&palette[in[x] * 4]) >> 16;
}
}
@@ -1022,7 +1022,7 @@ pa2l(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
int x;
/* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++, in += 4) {
- *out++ = L(&palette[in[0] * 4]) / 1000;
+ *out++ = L24(&palette[in[0] * 4]) >> 16;
}
}
@@ -1044,7 +1044,7 @@ p2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
/* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++, out += 4) {
const UINT8 *rgba = &palette[*in++ * 4];
- out[0] = out[1] = out[2] = L(rgba) / 1000;
+ out[0] = out[1] = out[2] = L24(rgba) >> 16;
out[3] = rgba[3];
}
}
@@ -1054,7 +1054,7 @@ pa2la(UINT8 *out, const UINT8 *in, int xsize, const UINT8 *palette) {
int x;
/* FIXME: precalculate greyscale palette? */
for (x = 0; x < xsize; x++, in += 4, out += 4) {
- out[0] = out[1] = out[2] = L(&palette[in[0] * 4]) / 1000;
+ out[0] = out[1] = out[2] = L24(&palette[in[0] * 4]) >> 16;
out[3] = in[3];
}
}
@@ -1063,7 +1063,7 @@ static void
p2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) {
int x;
for (x = 0; x < xsize; x++, out_ += 4) {
- INT32 v = L(&palette[in[x] * 4]) / 1000;
+ INT32 v = L24(&palette[in[x] * 4]) >> 16;
memcpy(out_, &v, sizeof(v));
}
}
@@ -1073,7 +1073,7 @@ pa2i(UINT8 *out_, const UINT8 *in, int xsize, const UINT8 *palette) {
int x;
INT32 *out = (INT32 *)out_;
for (x = 0; x < xsize; x++, in += 4) {
- *out++ = L(&palette[in[0] * 4]) / 1000;
+ *out++ = L24(&palette[in[0] * 4]) >> 16;
}
}
@@ -1594,9 +1594,8 @@ convert(
#ifdef notdef
return (Imaging)ImagingError_ValueError("conversion not supported");
#else
- static char buf[256];
- /* FIXME: may overflow if mode is too large */
- sprintf(buf, "conversion from %s to %s not supported", imIn->mode, mode);
+ static char buf[100];
+ snprintf(buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode);
return (Imaging)ImagingError_ValueError(buf);
#endif
}
@@ -1645,11 +1644,11 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) {
}
#else
{
- static char buf[256];
- /* FIXME: may overflow if mode is too large */
- sprintf(
+ static char buf[100];
+ snprintf(
buf,
- "conversion from %s to %s not supported in convert_transparent",
+ 100,
+ "conversion from %.10s to %.10s not supported in convert_transparent",
imIn->mode,
mode);
return (Imaging)ImagingError_ValueError(buf);
diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c
index b6f63b7e8..0e4899b5b 100644
--- a/src/libImaging/Draw.c
+++ b/src/libImaging/Draw.c
@@ -444,7 +444,7 @@ draw_horizontal_lines(
* Filled polygon draw function using scan line algorithm.
*/
static inline int
-polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline) {
+polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler hline, int hasAlpha) {
Edge **edge_table;
float *xx;
int edge_count = 0;
@@ -471,6 +471,9 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h
ymax = e[i].ymax;
}
if (e[i].ymin == e[i].ymax) {
+ if (hasAlpha != 1) {
+ (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink);
+ }
continue;
}
edge_table[edge_count++] = (e + i);
@@ -491,7 +494,6 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h
}
for (; ymin <= ymax; ymin++) {
int j = 0;
- int x_pos = 0;
for (i = 0; i < edge_count; i++) {
Edge *current = edge_table[i];
if (ymin >= current->ymin && ymin <= current->ymax) {
@@ -504,31 +506,38 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h
}
}
qsort(xx, j, sizeof(float), x_cmp);
- for (i = 1; i < j; i += 2) {
- int x_end = ROUND_DOWN(xx[i]);
- if (x_end < x_pos) {
- // Line would be before the current position
- continue;
- }
- draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
- if (x_end < x_pos) {
- // Line would be before the current position
- continue;
- }
-
- int x_start = ROUND_UP(xx[i - 1]);
- if (x_pos > x_start) {
- // Line would be partway through x_pos, so increase the starting point
- x_start = x_pos;
- if (x_end < x_start) {
- // Line would now end before it started
+ if (hasAlpha == 1) {
+ int x_pos = 0;
+ for (i = 1; i < j; i += 2) {
+ int x_end = ROUND_DOWN(xx[i]);
+ if (x_end < x_pos) {
+ // Line would be before the current position
continue;
}
+ draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
+ if (x_end < x_pos) {
+ // Line would be before the current position
+ continue;
+ }
+
+ int x_start = ROUND_UP(xx[i - 1]);
+ if (x_pos > x_start) {
+ // Line would be partway through x_pos, so increase the starting point
+ x_start = x_pos;
+ if (x_end < x_start) {
+ // Line would now end before it started
+ continue;
+ }
+ }
+ (*hline)(im, x_start, ymin, x_end, ink);
+ x_pos = x_end + 1;
+ }
+ draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
+ } else {
+ for (i = 1; i < j; i += 2) {
+ (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink);
}
- (*hline)(im, x_start, ymin, x_end, ink);
- x_pos = x_end + 1;
}
- draw_horizontal_lines(im, n, e, ink, &x_pos, ymin, hline);
}
free(xx);
@@ -538,17 +547,17 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, hline_handler h
static inline int
polygon8(Imaging im, int n, Edge *e, int ink, int eofill) {
- return polygon_generic(im, n, e, ink, eofill, hline8);
+ return polygon_generic(im, n, e, ink, eofill, hline8, 0);
}
static inline int
polygon32(Imaging im, int n, Edge *e, int ink, int eofill) {
- return polygon_generic(im, n, e, ink, eofill, hline32);
+ return polygon_generic(im, n, e, ink, eofill, hline32, 0);
}
static inline int
polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) {
- return polygon_generic(im, n, e, ink, eofill, hline32rgba);
+ return polygon_generic(im, n, e, ink, eofill, hline32rgba, 1);
}
static inline void
@@ -733,8 +742,8 @@ ImagingDrawRectangle(
}
int
-ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, int op) {
- int i, n;
+ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, int width, int op) {
+ int i, n, x0, y0, x1, y1;
DRAW *draw;
INT32 ink;
@@ -753,20 +762,45 @@ ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, i
return -1;
}
for (i = n = 0; i < count - 1; i++) {
- add_edge(&e[n++], xy[i + i], xy[i + i + 1], xy[i + i + 2], xy[i + i + 3]);
+ x0 = xy[i * 2];
+ y0 = xy[i * 2 + 1];
+ x1 = xy[i * 2 + 2];
+ y1 = xy[i * 2 + 3];
+ if (y0 == y1 && i != 0 && y0 == xy[i * 2 - 1]) {
+ // This is a horizontal line,
+ // that immediately follows another horizontal line
+ Edge *last_e = &e[n-1];
+ if (x1 > x0 && x0 > xy[i * 2 - 2]) {
+ // They are both increasing in x
+ last_e->xmax = x1;
+ continue;
+ } else if (x1 < x0 && x0 < xy[i * 2 - 2]) {
+ // They are both decreasing in x
+ last_e->xmin = x1;
+ continue;
+ }
+ }
+ add_edge(&e[n++], x0, y0, x1, y1);
}
- if (xy[i + i] != xy[0] || xy[i + i + 1] != xy[1]) {
- add_edge(&e[n++], xy[i + i], xy[i + i + 1], xy[0], xy[1]);
+ if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) {
+ add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]);
}
draw->polygon(im, n, e, ink, 0);
free(e);
} else {
/* Outline */
- for (i = 0; i < count - 1; i++) {
- draw->line(im, xy[i + i], xy[i + i + 1], xy[i + i + 2], xy[i + i + 3], ink);
+ if (width == 1) {
+ for (i = 0; i < count - 1; i++) {
+ draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink);
+ }
+ draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink);
+ } else {
+ for (i = 0; i < count - 1; i++) {
+ ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink_, width, op);
+ }
+ ImagingDrawWideLine(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink_, width, op);
}
- draw->line(im, xy[i + i], xy[i + i + 1], xy[0], xy[1], ink);
}
return 0;
@@ -1347,6 +1381,22 @@ pie_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float
s->root->l = lc;
s->root->r = rc;
s->root->type = ar - al < 180 ? CT_AND : CT_OR;
+
+ // add one more semiplane to avoid spikes
+ if (ar - al < 90) {
+ clip_node *old_root = s->root;
+ clip_node *spike_clipper = s->nodes + s->node_count++;
+ s->root = s->nodes + s->node_count++;
+ s->root->l = old_root;
+ s->root->r = spike_clipper;
+ s->root->type = CT_AND;
+
+ spike_clipper->l = spike_clipper->r = NULL;
+ spike_clipper->type = CT_CLIP;
+ spike_clipper->a = (xl + xr) / 2.0;
+ spike_clipper->b = (yl + yr) / 2.0;
+ spike_clipper->c = 0;
+ }
}
void
@@ -1804,14 +1854,8 @@ ImagingOutlineTransform(ImagingOutline outline, double a[6]) {
eIn = outline->edges;
n = outline->count;
- /* FIXME: ugly! */
- outline->edges = NULL;
- outline->count = outline->size = 0;
-
eOut = allocate(outline, n);
if (!eOut) {
- outline->edges = eIn;
- outline->count = outline->size = n;
ImagingError_MemoryError();
return -1;
}
@@ -1847,7 +1891,11 @@ ImagingOutlineTransform(ImagingOutline outline, double a[6]) {
eOut++;
}
- free(eIn);
+ free(outline->edges);
+
+ /* FIXME: ugly! */
+ outline->edges = NULL;
+ outline->count = outline->size = 0;
return 0;
}
diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c
index e9000fc99..d6e4ea0ff 100644
--- a/src/libImaging/FliDecode.c
+++ b/src/libImaging/FliDecode.c
@@ -46,7 +46,8 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
ptr = buf;
framesize = I32(ptr);
- if (framesize < I32(ptr)) {
+ // there can be one pad byte in the framesize
+ if (bytes + (bytes % 2) < framesize) {
return 0;
}
@@ -223,8 +224,15 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
break;
case 16:
/* COPY chunk */
- if (state->xsize > bytes / state->ysize) {
+ if (INT32_MAX / state->xsize < state->ysize) {
+ /* Integer overflow, bail */
+ state->errcode = IMAGING_CODEC_OVERRUN;
+ return -1;
+ }
+ /* Note, have to check Data + size, not just ptr + size) */
+ if (data + (state->xsize * state->ysize) > ptr + bytes) {
/* not enough data for frame */
+ /* UNDONE Unclear that we're actually going to leave the buffer at the right place. */
return ptr - buf; /* bytes consumed */
}
for (y = 0; y < state->ysize; y++) {
@@ -243,6 +251,11 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
return -1;
}
advance = I32(ptr);
+ if (advance == 0 ) {
+ // If there's no advance, we're in an infinite loop
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return -1;
+ }
if (advance < 0 || advance > bytes) {
state->errcode = IMAGING_CODEC_OVERRUN;
return -1;
diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h
index a85ce2b6e..4029bbfe5 100644
--- a/src/libImaging/Gif.h
+++ b/src/libImaging/Gif.h
@@ -9,10 +9,10 @@
/* Max size for a LZW code word. */
-#define GIFBITS 12
+#define GIFBITS 12
-#define GIFTABLE (1 << GIFBITS)
-#define GIFBUFFER (1 << GIFBITS)
+#define GIFTABLE (1<next < GIFTABLE) {
/* We'll only add this symbol if we have room
- for it (take advise, Netscape!) */
+ for it (take the advice, Netscape!) */
context->data[context->next] = c;
context->link[context->next] = context->lastcode;
@@ -248,8 +248,6 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t
/* To squeeze some extra pixels out of this loop, we test for
some common cases and handle them separately. */
- /* FIXME: should we handle the transparency index in here??? */
-
if (i == 1) {
if (state->x < state->xsize - 1) {
/* Single pixel, not at the end of the line. */
diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c
index e9064b3f5..f23245405 100644
--- a/src/libImaging/GifEncode.c
+++ b/src/libImaging/GifEncode.c
@@ -10,6 +10,7 @@
* 98-07-09 fl added interlace write support
* 99-02-07 fl rewritten, now uses a run-length encoding strategy
* 99-02-08 fl improved run-length encoding for long runs
+ * 2020-12-12 rdg Reworked for LZW compression.
*
* Copyright (c) Secret Labs AB 1997-99.
* Copyright (c) Fredrik Lundh 1997.
@@ -21,136 +22,206 @@
#include "Gif.h"
-/* codes from 0 to 255 are literals */
-#define CLEAR_CODE 256
-#define EOF_CODE 257
-#define FIRST_CODE 258
-#define LAST_CODE 511
+enum { INIT, ENCODE, FINISH };
-enum { INIT, ENCODE, ENCODE_EOF, FLUSH, EXIT };
+/* GIF LZW encoder by Raymond Gardner. */
+/* Released here under PIL license. */
-/* to make things a little less complicated, we use a simple output
- queue to hold completed blocks. the following inlined function
- adds a byte to the current block. it allocates a new block if
- necessary. */
+/* This LZW encoder conforms to the GIF LZW format specified in the original
+ * Compuserve GIF 87a and GIF 89a specifications (see e.g.
+ * https://www.w3.org/Graphics/GIF/spec-gif87.txt Appendix C and
+ * https://www.w3.org/Graphics/GIF/spec-gif89a.txt Appendix F).
+ */
-static inline int
-emit(GIFENCODERSTATE *context, int byte) {
- /* write a byte to the output buffer */
+/* Return values */
+#define GLZW_OK 0
+#define GLZW_NO_INPUT_AVAIL 1
+#define GLZW_NO_OUTPUT_AVAIL 2
+#define GLZW_INTERNAL_ERROR 3
- if (!context->block || context->block->size == 255) {
- GIFENCODERBLOCK *block;
+#define CODE_LIMIT 4096
- /* no room in the current block (or no current block);
- allocate a new one */
+/* Values of entry_state */
+enum { LZW_INITIAL, LZW_TRY_IN1, LZW_TRY_IN2, LZW_TRY_OUT1, LZW_TRY_OUT2,
+ LZW_FINISHED };
- /* add current block to end of flush queue */
- if (context->block) {
- block = context->flush;
- while (block && block->next) {
- block = block->next;
- }
- if (block) {
- block->next = context->block;
- } else {
- context->flush = context->block;
- }
- }
+/* Values of control_state */
+enum { PUT_HEAD, PUT_INIT_CLEAR, PUT_CLEAR, PUT_LAST_HEAD, PUT_END };
- /* get a new block */
- if (context->free) {
- block = context->free;
- context->free = NULL;
- } else {
- /* malloc check ok, small constant allocation */
- block = malloc(sizeof(GIFENCODERBLOCK));
- if (!block) {
- return 0;
- }
- }
-
- block->size = 0;
- block->next = NULL;
-
- context->block = block;
- }
-
- /* write new byte to block */
- context->block->data[context->block->size++] = byte;
-
- return 1;
+static void glzwe_reset(GIFENCODERSTATE *st) {
+ st->next_code = st->end_code + 1;
+ st->max_code = 2 * st->clear_code - 1;
+ st->code_width = st->bits + 1;
+ memset(st->codes, 0, sizeof(st->codes));
}
-/* write a code word to the current block. this is a macro to make
- sure it's inlined on all platforms */
+static void glzwe_init(GIFENCODERSTATE *st) {
+ st->clear_code = 1 << st->bits;
+ st->end_code = st->clear_code + 1;
+ glzwe_reset(st);
+ st->entry_state = LZW_INITIAL;
+ st->buf_bits_left = 8;
+ st->code_buffer = 0;
+}
-#define EMIT(code) \
- { \
- context->bitbuffer |= ((INT32)(code)) << context->bitcount; \
- context->bitcount += 9; \
- while (context->bitcount >= 8) { \
- if (!emit(context, (UINT8)context->bitbuffer)) { \
- state->errcode = IMAGING_CODEC_MEMORY; \
- return 0; \
- } \
- context->bitbuffer >>= 8; \
- context->bitcount -= 8; \
- } \
- }
-
-/* write a run. we use a combination of literals and combinations of
- literals. this can give quite decent compression for images with
- long stretches of identical pixels. but remember: if you want
- really good compression, use another file format. */
-
-#define EMIT_RUN(label) \
- { \
- label: \
- while (context->count > 0) { \
- int run = 2; \
- EMIT(context->last); \
- context->count--; \
- if (state->count++ == LAST_CODE) { \
- EMIT(CLEAR_CODE); \
- state->count = FIRST_CODE; \
- goto label; \
- } \
- while (context->count >= run) { \
- EMIT(state->count - 1); \
- context->count -= run; \
- run++; \
- if (state->count++ == LAST_CODE) { \
- EMIT(CLEAR_CODE); \
- state->count = FIRST_CODE; \
- goto label; \
- } \
- } \
- if (context->count > 1) { \
- EMIT(state->count - 1 - (run - context->count)); \
- context->count = 0; \
- if (state->count++ == LAST_CODE) { \
- EMIT(CLEAR_CODE); \
- state->count = FIRST_CODE; \
- } \
- break; \
- } \
- } \
+static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr,
+ UINT32 *in_avail, UINT32 *out_avail,
+ UINT32 end_of_data) {
+ switch (st->entry_state) {
+
+ case LZW_TRY_IN1:
+get_first_byte:
+ if (!*in_avail) {
+ if (end_of_data) {
+ goto end_of_data;
+ }
+ st->entry_state = LZW_TRY_IN1;
+ return GLZW_NO_INPUT_AVAIL;
+ }
+ st->head = *in_ptr++;
+ (*in_avail)--;
+
+ case LZW_TRY_IN2:
+encode_loop:
+ if (!*in_avail) {
+ if (end_of_data) {
+ st->code = st->head;
+ st->put_state = PUT_LAST_HEAD;
+ goto put_code;
+ }
+ st->entry_state = LZW_TRY_IN2;
+ return GLZW_NO_INPUT_AVAIL;
+ }
+ st->tail = *in_ptr++;
+ (*in_avail)--;
+
+ /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */
+ /* Hash found experimentally to be pretty good. */
+ /* This works ONLY with TABLE_SIZE a power of 2. */
+ st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1);
+ while (st->codes[st->probe]) {
+ if ((st->codes[st->probe] & 0xFFFFF) ==
+ ((st->head << 8) | st->tail)) {
+ st->head = st->codes[st->probe] >> 20;
+ goto encode_loop;
+ } else {
+ /* Reprobe decrement must be nonzero and relatively prime to table
+ * size. So, any odd positive number for power-of-2 size. */
+ if ((st->probe -= ((st->tail << 2) | 1)) < 0) {
+ st->probe += TABLE_SIZE;
+ }
+ }
+ }
+ /* Key not found, probe is at empty slot. */
+ st->code = st->head;
+ st->put_state = PUT_HEAD;
+ goto put_code;
+insert_code_or_clear: /* jump here after put_code */
+ if (st->next_code < CODE_LIMIT) {
+ st->codes[st->probe] = (st->next_code << 20) |
+ (st->head << 8) | st->tail;
+ if (st->next_code > st->max_code) {
+ st->max_code = st->max_code * 2 + 1;
+ st->code_width++;
+ }
+ st->next_code++;
+ } else {
+ st->code = st->clear_code;
+ st->put_state = PUT_CLEAR;
+ goto put_code;
+reset_after_clear: /* jump here after put_code */
+ glzwe_reset(st);
+ }
+ st->head = st->tail;
+ goto encode_loop;
+
+ case LZW_INITIAL:
+ glzwe_reset(st);
+ st->code = st->clear_code;
+ st->put_state = PUT_INIT_CLEAR;
+put_code:
+ st->code_bits_left = st->code_width;
+check_buf_bits:
+ if (!st->buf_bits_left) { /* out buffer full */
+
+ case LZW_TRY_OUT1:
+ if (!*out_avail) {
+ st->entry_state = LZW_TRY_OUT1;
+ return GLZW_NO_OUTPUT_AVAIL;
+ }
+ *out_ptr++ = st->code_buffer;
+ (*out_avail)--;
+ st->code_buffer = 0;
+ st->buf_bits_left = 8;
+ }
+ /* code bits to pack */
+ UINT32 n = st->buf_bits_left < st->code_bits_left
+ ? st->buf_bits_left : st->code_bits_left;
+ st->code_buffer |=
+ (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left);
+ st->code >>= n;
+ st->buf_bits_left -= n;
+ st->code_bits_left -= n;
+ if (st->code_bits_left) {
+ goto check_buf_bits;
+ }
+ switch (st->put_state) {
+ case PUT_INIT_CLEAR:
+ goto get_first_byte;
+ case PUT_HEAD:
+ goto insert_code_or_clear;
+ case PUT_CLEAR:
+ goto reset_after_clear;
+ case PUT_LAST_HEAD:
+ goto end_of_data;
+ case PUT_END:
+ goto flush_code_buffer;
+ default:
+ return GLZW_INTERNAL_ERROR;
+ }
+
+end_of_data:
+ st->code = st->end_code;
+ st->put_state = PUT_END;
+ goto put_code;
+flush_code_buffer: /* jump here after put_code */
+ if (st->buf_bits_left < 8) {
+
+ case LZW_TRY_OUT2:
+ if (!*out_avail) {
+ st->entry_state = LZW_TRY_OUT2;
+ return GLZW_NO_OUTPUT_AVAIL;
+ }
+ *out_ptr++ = st->code_buffer;
+ (*out_avail)--;
+ }
+ st->entry_state = LZW_FINISHED;
+ return GLZW_OK;
+
+ case LZW_FINISHED:
+ return GLZW_OK;
+
+ default:
+ return GLZW_INTERNAL_ERROR;
}
+}
+/* -END- GIF LZW encoder. */
int
-ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
- UINT8 *ptr;
- int this;
+ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) {
+ UINT8* ptr;
+ UINT8* sub_block_ptr;
+ UINT8* sub_block_limit;
+ UINT8* buf_limit;
+ GIFENCODERSTATE *context = (GIFENCODERSTATE*) state->context;
+ int r;
- GIFENCODERBLOCK *block;
- GIFENCODERSTATE *context = (GIFENCODERSTATE *)state->context;
+ UINT32 in_avail, in_used;
+ UINT32 out_avail, out_used;
- if (!state->state) {
- /* place a clear code in the output buffer */
- context->bitbuffer = CLEAR_CODE;
- context->bitcount = 9;
-
- state->count = FIRST_CODE;
+ if (state->state == INIT) {
+ state->state = ENCODE;
+ glzwe_init(context);
if (context->interlace) {
context->interlace = 1;
@@ -159,166 +230,132 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
context->step = 1;
}
- context->last = -1;
-
+ /* Need at least 2 bytes for data sub-block; 5 for empty image */
+ if (bytes < 5) {
+ state->errcode = IMAGING_CODEC_CONFIG;
+ return 0;
+ }
/* sanity check */
if (state->xsize <= 0 || state->ysize <= 0) {
- state->state = ENCODE_EOF;
+ /* Is this better than an error return? */
+ /* This will handle any legal "LZW Minimum Code Size" */
+ memset(buf, 0, 5);
+ in_avail = 0;
+ out_avail = 5;
+ r = glzwe(context, (const UINT8 *)"", buf + 1, &in_avail, &out_avail, 1);
+ if (r == GLZW_OK) {
+ r = 5 - out_avail;
+ if (r < 1 || r > 3) {
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return 0;
+ }
+ buf[0] = r;
+ state->errcode = IMAGING_CODEC_END;
+ return r + 2;
+ } else {
+ /* Should not be possible unless something external to this
+ * routine messes with our state data */
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return 0;
+ }
}
+ /* Init state->x to make if() below true the first time through. */
+ state->x = state->xsize;
}
- ptr = buf;
+ buf_limit = buf + bytes;
+ sub_block_limit = sub_block_ptr = ptr = buf;
+ /* On entry, buf is output buffer, bytes is space available in buf.
+ * Loop here getting input until buf is full or image is all encoded. */
for (;;) {
- switch (state->state) {
- case INIT:
- case ENCODE:
+ /* Set up sub-block ptr and limit. sub_block_ptr stays at beginning
+ * of sub-block until it is full. ptr will advance when any data is
+ * placed in buf.
+ */
+ if (ptr >= sub_block_limit) {
+ if (buf_limit - ptr < 2) { /* Need at least 2 for data sub-block */
+ return ptr - buf;
+ }
+ sub_block_ptr = ptr;
+ sub_block_limit = sub_block_ptr +
+ (256 < buf_limit - sub_block_ptr ?
+ 256 : buf_limit - sub_block_ptr);
+ *ptr++ = 0;
+ }
- /* identify and store a run of pixels */
+ /* Get next row of pixels. */
+ /* This if() originally tested state->x==0 for the first time through.
+ * This no longer works, as the loop will not advance state->x if
+ * glzwe() does not consume any input; this would advance the row
+ * spuriously. Now pre-init state->x above for first time, and avoid
+ * entering if() when state->state is FINISH, or it will loop
+ * infinitely.
+ */
+ if (state->x >= state->xsize && state->state == ENCODE) {
+ if (!context->interlace && state->y >= state->ysize) {
+ state->state = FINISH;
+ continue;
+ }
- if (state->x == 0 || state->x >= state->xsize) {
- if (!context->interlace && state->y >= state->ysize) {
- state->state = ENCODE_EOF;
+ /* get another line of data */
+ state->shuffle(
+ state->buffer,
+ (UINT8*) im->image[state->y + state->yoff] +
+ state->xoff * im->pixelsize, state->xsize
+ );
+ state->x = 0;
+
+ /* step forward, according to the interlace settings */
+ state->y += context->step;
+ while (context->interlace && state->y >= state->ysize) {
+ switch (context->interlace) {
+ case 1:
+ state->y = 4;
+ context->interlace = 2;
break;
- }
-
- if (context->flush) {
- state->state = FLUSH;
+ case 2:
+ context->step = 4;
+ state->y = 2;
+ context->interlace = 3;
break;
- }
-
- /* get another line of data */
- state->shuffle(
- state->buffer,
- (UINT8 *)im->image[state->y + state->yoff] +
- state->xoff * im->pixelsize,
- state->xsize);
-
- state->x = 0;
-
- if (state->state == INIT) {
- /* preload the run-length buffer and get going */
- context->last = state->buffer[0];
- context->count = state->x = 1;
- state->state = ENCODE;
- }
-
- /* step forward, according to the interlace settings */
- state->y += context->step;
- while (context->interlace && state->y >= state->ysize)
- switch (context->interlace) {
- case 1:
- state->y = 4;
- context->interlace = 2;
- break;
- case 2:
- context->step = 4;
- state->y = 2;
- context->interlace = 3;
- break;
- case 3:
- context->step = 2;
- state->y = 1;
- context->interlace = 0;
- break;
- default:
- /* just make sure we don't loop forever */
- context->interlace = 0;
- }
- }
- /* Potential special case for xsize==1 */
- if (state->x < state->xsize) {
- this = state->buffer[state->x++];
- } else {
- EMIT_RUN(label0);
- break;
+ case 3:
+ context->step = 2;
+ state->y = 1;
+ context->interlace = 0;
+ break;
+ default:
+ /* just make sure we don't loop forever */
+ context->interlace = 0;
}
+ }
+ }
- if (this == context->last) {
- context->count++;
- } else {
- EMIT_RUN(label1);
- context->last = this;
- context->count = 1;
- }
- break;
+ in_avail = state->xsize - state->x; /* bytes left in line */
+ out_avail = sub_block_limit - ptr; /* bytes left in sub-block */
+ r = glzwe(context, &state->buffer[state->x], ptr, &in_avail,
+ &out_avail, state->state == FINISH);
+ out_used = sub_block_limit - ptr - out_avail;
+ *sub_block_ptr += out_used;
+ ptr += out_used;
+ in_used = state->xsize - state->x - in_avail;
+ state->x += in_used;
- case ENCODE_EOF:
-
- /* write the final run */
- EMIT_RUN(label2);
-
- /* write an end of image marker */
- EMIT(EOF_CODE);
-
- /* empty the bit buffer */
- while (context->bitcount > 0) {
- if (!emit(context, (UINT8)context->bitbuffer)) {
- state->errcode = IMAGING_CODEC_MEMORY;
- return 0;
- }
- context->bitbuffer >>= 8;
- context->bitcount -= 8;
- }
-
- /* flush the last block, and exit */
- if (context->block) {
- GIFENCODERBLOCK *block;
- block = context->flush;
- while (block && block->next) {
- block = block->next;
- }
- if (block) {
- block->next = context->block;
- } else {
- context->flush = context->block;
- }
- context->block = NULL;
- }
-
- state->state = EXIT;
-
- /* fall through... */
-
- case EXIT:
- case FLUSH:
-
- while (context->flush) {
- /* get a block from the flush queue */
- block = context->flush;
-
- if (block->size > 0) {
- /* make sure it fits into the output buffer */
- if (bytes < block->size + 1) {
- return ptr - buf;
- }
-
- ptr[0] = block->size;
- memcpy(ptr + 1, block->data, block->size);
-
- ptr += block->size + 1;
- bytes -= block->size + 1;
- }
-
- context->flush = block->next;
-
- if (context->free) {
- free(context->free);
- }
- context->free = block;
- }
-
- if (state->state == EXIT) {
- /* this was the last block! */
- if (context->free) {
- free(context->free);
- }
- state->errcode = IMAGING_CODEC_END;
- return ptr - buf;
- }
-
- state->state = ENCODE;
- break;
+ if (r == GLZW_OK) {
+ /* Should not be possible when end-of-data flag is false. */
+ state->errcode = IMAGING_CODEC_END;
+ return ptr - buf;
+ } else if (r == GLZW_NO_INPUT_AVAIL) {
+ /* Used all the input line; get another line */
+ continue;
+ } else if (r == GLZW_NO_OUTPUT_AVAIL) {
+ /* subblock is full */
+ continue;
+ } else {
+ /* Should not be possible unless something external to this
+ * routine messes with our state data */
+ state->errcode = IMAGING_CODEC_BROKEN;
+ return 0;
}
}
}
diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h
index 9a2060edf..af9996ca9 100644
--- a/src/libImaging/ImPlatform.h
+++ b/src/libImaging/ImPlatform.h
@@ -9,12 +9,6 @@
#include "Python.h"
-/* Workaround issue #2479 */
-#if PY_VERSION_HEX < 0x03070000 && defined(PySlice_GetIndicesEx) && \
- !defined(PYPY_VERSION)
-#undef PySlice_GetIndicesEx
-#endif
-
/* Check that we have an ANSI compliant compiler */
#ifndef HAVE_PROTOTYPES
#error Sorry, this library requires support for ANSI prototypes.
@@ -31,11 +25,18 @@
#endif
#endif
-#ifdef _WIN32
+#if defined(_WIN32) || defined(__CYGWIN__)
#define WIN32_LEAN_AND_MEAN
#include
+#ifdef __CYGWIN__
+#undef _WIN64
+#undef _WIN32
+#undef __WIN32__
+#undef WIN32
+#endif
+
#else
/* For System that are not Windows, we'll need to define these. */
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index ae323f390..9b1c1024d 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -370,7 +370,7 @@ ImagingTransform(
int y0,
int x1,
int y1,
- double *a,
+ double a[8],
int filter,
int fill);
extern Imaging
@@ -487,7 +487,7 @@ ImagingDrawPieslice(
extern int
ImagingDrawPoint(Imaging im, int x, int y, const void *ink, int op);
extern int
-ImagingDrawPolygon(Imaging im, int points, int *xy, const void *ink, int fill, int op);
+ImagingDrawPolygon(Imaging im, int points, int *xy, const void *ink, int fill, int width, int op);
extern int
ImagingDrawRectangle(
Imaging im,
diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h
index ad6f280ac..0c0c1eda9 100644
--- a/src/libImaging/ImagingUtils.h
+++ b/src/libImaging/ImagingUtils.h
@@ -29,7 +29,7 @@
/* This is to work around a bug in GCC prior 4.9 in 64 bit mode.
GCC generates code with partial dependency which is 3 times slower.
- See: http://stackoverflow.com/a/26588074/253146 */
+ See: https://stackoverflow.com/a/26588074/253146 */
#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \
!defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900)
static float __attribute__((always_inline)) inline _i2f(int v) {
diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c
index a2a7354db..8f27d87d8 100644
--- a/src/libImaging/Jpeg2KDecode.c
+++ b/src/libImaging/Jpeg2KDecode.c
@@ -73,6 +73,8 @@ struct j2k_decode_unpacker {
const char *mode;
OPJ_COLOR_SPACE color_space;
unsigned components;
+ /* bool indicating if unpacker supports subsampling */
+ int subsampling;
j2k_unpacker_t unpacker;
};
@@ -178,9 +180,11 @@ j2ku_gray_i(
case 2:
for (y = 0; y < h; ++y) {
const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w];
- UINT16 *row = (UINT16 *)im->image[y0 + y] + x0;
+ UINT8 *row = (UINT8 *)im->image[y0 + y] + x0;
for (x = 0; x < w; ++x) {
- *row++ = j2ku_shift(offset + *data++, shift);
+ UINT16 pixel = j2ku_shift(offset + *data++, shift);
+ *row++ = pixel;
+ *row++ = pixel >> 8;
}
}
break;
@@ -350,6 +354,7 @@ j2ku_srgb_rgb(
unsigned h = tileinfo->y1 - tileinfo->y0;
int shifts[3], offsets[3], csiz[3];
+ unsigned dx[3], dy[3];
const UINT8 *cdata[3];
const UINT8 *cptr = tiledata;
unsigned n, x, y;
@@ -359,6 +364,8 @@ j2ku_srgb_rgb(
shifts[n] = 8 - in->comps[n].prec;
offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0;
csiz[n] = (in->comps[n].prec + 7) >> 3;
+ dx[n] = (in->comps[n].dx);
+ dy[n] = (in->comps[n].dy);
if (csiz[n] == 3) {
csiz[n] = 4;
@@ -368,14 +375,14 @@ j2ku_srgb_rgb(
offsets[n] += 1 << (-shifts[n] - 1);
}
- cptr += csiz[n] * w * h;
+ cptr += csiz[n] * (w / dx[n]) * (h / dy[n]);
}
for (y = 0; y < h; ++y) {
const UINT8 *data[3];
UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4;
for (n = 0; n < 3; ++n) {
- data[n] = &cdata[n][csiz[n] * y * w];
+ data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])];
}
for (x = 0; x < w; ++x) {
@@ -384,15 +391,13 @@ j2ku_srgb_rgb(
switch (csiz[n]) {
case 1:
- word = *data[n]++;
+ word = data[n][x / dx[n]];
break;
case 2:
- word = *(const UINT16 *)data[n];
- data[n] += 2;
+ word = ((const UINT16 *)data[n])[x / dx[n]];
break;
case 4:
- word = *(const UINT32 *)data[n];
- data[n] += 4;
+ word = ((const UINT32 *)data[n])[x / dx[n]];
break;
}
@@ -415,6 +420,7 @@ j2ku_sycc_rgb(
unsigned h = tileinfo->y1 - tileinfo->y0;
int shifts[3], offsets[3], csiz[3];
+ unsigned dx[3], dy[3];
const UINT8 *cdata[3];
const UINT8 *cptr = tiledata;
unsigned n, x, y;
@@ -424,6 +430,8 @@ j2ku_sycc_rgb(
shifts[n] = 8 - in->comps[n].prec;
offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0;
csiz[n] = (in->comps[n].prec + 7) >> 3;
+ dx[n] = (in->comps[n].dx);
+ dy[n] = (in->comps[n].dy);
if (csiz[n] == 3) {
csiz[n] = 4;
@@ -433,7 +441,7 @@ j2ku_sycc_rgb(
offsets[n] += 1 << (-shifts[n] - 1);
}
- cptr += csiz[n] * w * h;
+ cptr += csiz[n] * (w / dx[n]) * (h / dy[n]);
}
for (y = 0; y < h; ++y) {
@@ -441,7 +449,7 @@ j2ku_sycc_rgb(
UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4;
UINT8 *row_start = row;
for (n = 0; n < 3; ++n) {
- data[n] = &cdata[n][csiz[n] * y * w];
+ data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])];
}
for (x = 0; x < w; ++x) {
@@ -450,15 +458,13 @@ j2ku_sycc_rgb(
switch (csiz[n]) {
case 1:
- word = *data[n]++;
+ word = data[n][x / dx[n]];
break;
case 2:
- word = *(const UINT16 *)data[n];
- data[n] += 2;
+ word = ((const UINT16 *)data[n])[x / dx[n]];
break;
case 4:
- word = *(const UINT32 *)data[n];
- data[n] += 4;
+ word = ((const UINT32 *)data[n])[x / dx[n]];
break;
}
@@ -483,6 +489,7 @@ j2ku_srgba_rgba(
unsigned h = tileinfo->y1 - tileinfo->y0;
int shifts[4], offsets[4], csiz[4];
+ unsigned dx[4], dy[4];
const UINT8 *cdata[4];
const UINT8 *cptr = tiledata;
unsigned n, x, y;
@@ -492,6 +499,8 @@ j2ku_srgba_rgba(
shifts[n] = 8 - in->comps[n].prec;
offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0;
csiz[n] = (in->comps[n].prec + 7) >> 3;
+ dx[n] = (in->comps[n].dx);
+ dy[n] = (in->comps[n].dy);
if (csiz[n] == 3) {
csiz[n] = 4;
@@ -501,14 +510,14 @@ j2ku_srgba_rgba(
offsets[n] += 1 << (-shifts[n] - 1);
}
- cptr += csiz[n] * w * h;
+ cptr += csiz[n] * (w / dx[n]) * (h / dy[n]);
}
for (y = 0; y < h; ++y) {
const UINT8 *data[4];
UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4;
for (n = 0; n < 4; ++n) {
- data[n] = &cdata[n][csiz[n] * y * w];
+ data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])];
}
for (x = 0; x < w; ++x) {
@@ -517,15 +526,13 @@ j2ku_srgba_rgba(
switch (csiz[n]) {
case 1:
- word = *data[n]++;
+ word = data[n][x / dx[n]];
break;
case 2:
- word = *(const UINT16 *)data[n];
- data[n] += 2;
+ word = ((const UINT16 *)data[n])[x / dx[n]];
break;
case 4:
- word = *(const UINT32 *)data[n];
- data[n] += 4;
+ word = ((const UINT32 *)data[n])[x / dx[n]];
break;
}
@@ -547,6 +554,7 @@ j2ku_sycca_rgba(
unsigned h = tileinfo->y1 - tileinfo->y0;
int shifts[4], offsets[4], csiz[4];
+ unsigned dx[4], dy[4];
const UINT8 *cdata[4];
const UINT8 *cptr = tiledata;
unsigned n, x, y;
@@ -556,6 +564,8 @@ j2ku_sycca_rgba(
shifts[n] = 8 - in->comps[n].prec;
offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0;
csiz[n] = (in->comps[n].prec + 7) >> 3;
+ dx[n] = (in->comps[n].dx);
+ dy[n] = (in->comps[n].dy);
if (csiz[n] == 3) {
csiz[n] = 4;
@@ -565,7 +575,7 @@ j2ku_sycca_rgba(
offsets[n] += 1 << (-shifts[n] - 1);
}
- cptr += csiz[n] * w * h;
+ cptr += csiz[n] * (w / dx[n]) * (h / dy[n]);
}
for (y = 0; y < h; ++y) {
@@ -573,7 +583,7 @@ j2ku_sycca_rgba(
UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4;
UINT8 *row_start = row;
for (n = 0; n < 4; ++n) {
- data[n] = &cdata[n][csiz[n] * y * w];
+ data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])];
}
for (x = 0; x < w; ++x) {
@@ -582,15 +592,13 @@ j2ku_sycca_rgba(
switch (csiz[n]) {
case 1:
- word = *data[n]++;
+ word = data[n][x / dx[n]];
break;
case 2:
- word = *(const UINT16 *)data[n];
- data[n] += 2;
+ word = ((const UINT16 *)data[n])[x / dx[n]];
break;
case 4:
- word = *(const UINT32 *)data[n];
- data[n] += 4;
+ word = ((const UINT32 *)data[n])[x / dx[n]];
break;
}
@@ -604,22 +612,22 @@ j2ku_sycca_rgba(
}
static const struct j2k_decode_unpacker j2k_unpackers[] = {
- {"L", OPJ_CLRSPC_GRAY, 1, j2ku_gray_l},
- {"I;16", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i},
- {"I;16B", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i},
- {"LA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la},
- {"RGB", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb},
- {"RGB", OPJ_CLRSPC_GRAY, 2, j2ku_gray_rgb},
- {"RGB", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb},
- {"RGB", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb},
- {"RGB", OPJ_CLRSPC_SRGB, 4, j2ku_srgb_rgb},
- {"RGB", OPJ_CLRSPC_SYCC, 4, j2ku_sycc_rgb},
- {"RGBA", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb},
- {"RGBA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la},
- {"RGBA", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb},
- {"RGBA", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb},
- {"RGBA", OPJ_CLRSPC_SRGB, 4, j2ku_srgba_rgba},
- {"RGBA", OPJ_CLRSPC_SYCC, 4, j2ku_sycca_rgba},
+ {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l},
+ {"I;16", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i},
+ {"I;16B", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i},
+ {"LA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la},
+ {"RGB", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb},
+ {"RGB", OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb},
+ {"RGB", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb},
+ {"RGB", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb},
+ {"RGB", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb},
+ {"RGB", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb},
+ {"RGBA", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb},
+ {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la},
+ {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb},
+ {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb},
+ {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba},
+ {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba},
};
/* -------------------------------------------------------------------- */
@@ -644,7 +652,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
j2k_unpacker_t unpack = NULL;
size_t buffer_size = 0, tile_bytes = 0;
unsigned n, tile_height, tile_width;
- int components;
+ int subsampling;
+ int total_component_width = 0;
stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE);
@@ -706,11 +715,16 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
goto quick_exit;
}
- for (n = 1; n < image->numcomps; ++n) {
+ /*
+ * Find first component with subsampling.
+ *
+ * This is a heuristic to determine the colorspace if unspecified.
+ */
+ subsampling = -1;
+ for (n = 0; n < image->numcomps; ++n) {
if (image->comps[n].dx != 1 || image->comps[n].dy != 1) {
- state->errcode = IMAGING_CODEC_BROKEN;
- state->state = J2K_STATE_FAILED;
- goto quick_exit;
+ subsampling = n;
+ break;
}
}
@@ -726,12 +740,14 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
If colorspace is unspecified, we assume:
- Number of components Colorspace
- -----------------------------------------
- 1 gray
- 2 gray (+ alpha)
- 3 sRGB
- 4 sRGB (+ alpha)
+ Number of components Subsampling Colorspace
+ -------------------------------------------------------
+ 1 Any gray
+ 2 Any gray (+ alpha)
+ 3 -1, 0 sRGB
+ 3 1, 2 YCbCr
+ 4 -1, 0, 3 sRGB (+ alpha)
+ 4 1, 2 YCbCr (+ alpha)
*/
@@ -746,14 +762,25 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
break;
case 3:
case 4:
- color_space = OPJ_CLRSPC_SRGB;
- break;
+ switch (subsampling) {
+ case -1:
+ case 0:
+ case 3:
+ color_space = OPJ_CLRSPC_SRGB;
+ break;
+ case 1:
+ case 2:
+ color_space = OPJ_CLRSPC_SYCC;
+ break;
+ }
+ break;
}
}
for (n = 0; n < sizeof(j2k_unpackers) / sizeof(j2k_unpackers[0]); ++n) {
if (color_space == j2k_unpackers[n].color_space &&
image->numcomps == j2k_unpackers[n].components &&
+ (j2k_unpackers[n].subsampling || (subsampling == -1)) &&
strcmp(im->mode, j2k_unpackers[n].mode) == 0) {
unpack = j2k_unpackers[n].unpacker;
break;
@@ -814,23 +841,40 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
goto quick_exit;
}
- /* Sometimes the tile_info.datasize we get back from openjpeg
- is less than numcomps*w*h, and we overflow in the
- shuffle stage */
-
- tile_width = tile_info.x1 - tile_info.x0;
- tile_height = tile_info.y1 - tile_info.y0;
- components = tile_info.nb_comps == 3 ? 4 : tile_info.nb_comps;
- if ((tile_width > UINT_MAX / components) ||
- (tile_height > UINT_MAX / components) ||
- (tile_width > UINT_MAX / (tile_height * components)) ||
- (tile_height > UINT_MAX / (tile_width * components))) {
+ if (tile_info.nb_comps != image->numcomps) {
state->errcode = IMAGING_CODEC_BROKEN;
state->state = J2K_STATE_FAILED;
goto quick_exit;
}
- tile_bytes = tile_width * tile_height * components;
+ /* Sometimes the tile_info.datasize we get back from openjpeg
+ is less than sum(comp_bytes)*w*h, and we overflow in the
+ shuffle stage */
+
+ tile_width = tile_info.x1 - tile_info.x0;
+ tile_height = tile_info.y1 - tile_info.y0;
+
+ /* Total component width = sum (component_width) e.g, it's
+ legal for an la file to have a 1 byte width for l, and 4 for
+ a, and then a malicious file could have a smaller tile_bytes
+ */
+
+ for (n=0; n < tile_info.nb_comps; n++) {
+ // see csize /acsize calcs
+ int csize = (image->comps[n].prec + 7) >> 3;
+ csize = (csize == 3) ? 4 : csize;
+ total_component_width += csize;
+ }
+ if ((tile_width > UINT_MAX / total_component_width) ||
+ (tile_height > UINT_MAX / total_component_width) ||
+ (tile_width > UINT_MAX / (tile_height * total_component_width)) ||
+ (tile_height > UINT_MAX / (tile_width * total_component_width))) {
+ state->errcode = IMAGING_CODEC_BROKEN;
+ state->state = J2K_STATE_FAILED;
+ goto quick_exit;
+ }
+
+ tile_bytes = tile_width * tile_height * total_component_width;
if (tile_bytes > tile_info.data_size) {
tile_info.data_size = tile_bytes;
@@ -844,6 +888,10 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) {
state->state = J2K_STATE_FAILED;
goto quick_exit;
}
+ /* Undefined behavior, sometimes decode_tile_data doesn't
+ fill the buffer and we do things with it later, leading
+ to valgrind errors. */
+ memset(new, 0, tile_info.data_size);
state->buffer = new;
buffer_size = tile_info.data_size;
}
diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c
index 2e6b5daf0..86cd7d5af 100644
--- a/src/libImaging/Jpeg2KEncode.c
+++ b/src/libImaging/Jpeg2KEncode.c
@@ -110,8 +110,15 @@ j2k_pack_i16(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsig
for (y = 0; y < h; ++y) {
UINT8 *data = (UINT8 *)(im->image[y + y0] + x0);
for (x = 0; x < w; ++x) {
- *ptr++ = *data++;
- *ptr++ = *data++;
+#ifdef WORDS_BIGENDIAN
+ ptr[0] = data[1];
+ ptr[1] = data[0];
+#else
+ ptr[0] = data[0];
+ ptr[1] = data[1];
+#endif
+ ptr += 2;
+ data += 2;
}
}
}
@@ -301,13 +308,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
components = 1;
color_space = OPJ_CLRSPC_GRAY;
pack = j2k_pack_l;
- } else if (strcmp(im->mode, "I;16") == 0) {
- components = 1;
- color_space = OPJ_CLRSPC_GRAY;
- pack = j2k_pack_i16;
- prec = 16;
- bpp = 12;
- } else if (strcmp(im->mode, "I;16B") == 0) {
+ } else if (strcmp(im->mode, "I;16") == 0 || strcmp(im->mode, "I;16B") == 0) {
components = 1;
color_space = OPJ_CLRSPC_GRAY;
pack = j2k_pack_i16;
@@ -458,6 +459,12 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
break;
}
+ if (!context->num_resolutions) {
+ while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) {
+ params.numresolution -= 1;
+ }
+ }
+
if (context->cinema_mode != OPJ_OFF) {
j2k_set_cinema_params(im, components, ¶ms);
}
diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c
index 2fdee919f..0c7c0497e 100644
--- a/src/libImaging/Pack.c
+++ b/src/libImaging/Pack.c
@@ -656,7 +656,11 @@ static struct {
/* storage modes */
{"I;16", "I;16", 16, copy2},
+#ifdef WORDS_BIGENDIAN
+ {"I;16", "I;16B", 16, packI16N_I16},
+#else
{"I;16", "I;16B", 16, packI16N_I16B},
+#endif
{"I;16B", "I;16B", 16, copy2},
{"I;16L", "I;16L", 16, copy2},
{"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian.
diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c
index 03b17f571..be26cd260 100644
--- a/src/libImaging/Paste.c
+++ b/src/libImaging/Paste.c
@@ -417,9 +417,16 @@ fill_mask_L(
if (imOut->image8) {
for (y = 0; y < ysize; y++) {
UINT8 *out = imOut->image8[y + dy] + dx;
+ if (strncmp(imOut->mode, "I;16", 4) == 0) {
+ out += dx;
+ }
UINT8 *mask = imMask->image8[y + sy] + sx;
for (x = 0; x < xsize; x++) {
*out = BLEND(*mask, *out, ink[0], tmp1);
+ if (strncmp(imOut->mode, "I;16", 4) == 0) {
+ out++;
+ *out = BLEND(*mask, *out, ink[0], tmp1);
+ }
out++, mask++;
}
}
@@ -436,7 +443,7 @@ fill_mask_L(
strcmp(imOut->mode, "La") == 0 ||
strcmp(imOut->mode, "LA") == 0 ||
strcmp(imOut->mode, "PA") == 0) &&
- i != 3) {
+ i != 3 && channel_mask != 0) {
channel_mask =
255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255);
}
diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c
index f5a5d567c..1c6b9d6a2 100644
--- a/src/libImaging/Quant.c
+++ b/src/libImaging/Quant.c
@@ -753,11 +753,19 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) {
return 1;
}
+typedef struct {
+ uint32_t *distance;
+ uint32_t index;
+} DistanceWithIndex;
+
static int
-_sort_ulong_ptr_keys(const void *a, const void *b) {
- uint32_t A = **(uint32_t **)a;
- uint32_t B = **(uint32_t **)b;
- return (A == B) ? 0 : ((A < B) ? -1 : +1);
+_distance_index_cmp(const void *a, const void *b) {
+ DistanceWithIndex *A = (DistanceWithIndex *)a;
+ DistanceWithIndex *B = (DistanceWithIndex *)b;
+ if (*A->distance == *B->distance) {
+ return A->index < B->index ? -1 : +1;
+ }
+ return *A->distance < *B->distance ? -1 : +1;
}
static int
@@ -789,10 +797,11 @@ resort_distance_tables(
return 1;
}
-static void
+static int
build_distance_tables(
uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) {
uint32_t i, j;
+ DistanceWithIndex *dwi;
for (i = 0; i < nEntries; i++) {
avgDist[i * nEntries + i] = 0;
@@ -804,13 +813,29 @@ build_distance_tables(
avgDistSortKey[i * nEntries + j] = &(avgDist[i * nEntries + j]);
}
}
- for (i = 0; i < nEntries; i++) {
- qsort(
- avgDistSortKey + i * nEntries,
- nEntries,
- sizeof(uint32_t *),
- _sort_ulong_ptr_keys);
+
+ dwi = calloc(nEntries, sizeof(DistanceWithIndex));
+ if (!dwi) {
+ return 0;
}
+ for (i = 0; i < nEntries; i++) {
+ for (j = 0; j < nEntries; j++) {
+ dwi[j] = (DistanceWithIndex){
+ &(avgDist[i * nEntries + j]),
+ j
+ };
+ }
+ qsort(
+ dwi,
+ nEntries,
+ sizeof(DistanceWithIndex),
+ _distance_index_cmp);
+ for (j = 0; j < nEntries; j++) {
+ avgDistSortKey[i * nEntries + j] = dwi[j].distance;
+ }
+ }
+ free(dwi);
+ return 1;
}
static int
@@ -1175,8 +1200,10 @@ k_means(
if (!built) {
compute_palette_from_quantized_pixels(
pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp);
- build_distance_tables(
- avgDist, avgDistSortKey, paletteData, nPaletteEntries);
+ if (!build_distance_tables(
+ avgDist, avgDistSortKey, paletteData, nPaletteEntries)) {
+ goto error_3;
+ }
built = 1;
} else {
recompute_palette_from_averages(paletteData, nPaletteEntries, avg, count);
@@ -1243,7 +1270,7 @@ error_1:
return 0;
}
-int
+static int
quantize(
Pixel *pixelData,
uint32_t nPixels,
@@ -1372,7 +1399,9 @@ quantize(
goto error_6;
}
- build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries);
+ if (!build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries)) {
+ goto error_7;
+ }
if (!map_image_pixels_from_median_box(
pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) {
@@ -1511,7 +1540,7 @@ compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u
}
}
-int
+static int
quantize2(
Pixel *pixelData,
uint32_t nPixels,
@@ -1577,7 +1606,9 @@ quantize2(
goto error_3;
}
- build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels);
+ if (!build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels)) {
+ goto error_4;
+ }
if (!map_image_pixels(
pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) {
@@ -1683,9 +1714,26 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
} else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) {
/* true colour */
+ withAlpha = !strcmp(im->mode, "RGBA");
+ int transparency = 0;
+ unsigned char r, g, b;
for (i = y = 0; y < im->ysize; y++) {
for (x = 0; x < im->xsize; x++, i++) {
p[i].v = im->image32[y][x];
+ if (withAlpha && p[i].c.a == 0) {
+ if (transparency == 0) {
+ transparency = 1;
+ r = p[i].c.r;
+ g = p[i].c.g;
+ b = p[i].c.b;
+ } else {
+ /* Set all subsequent transparent pixels
+ to the same colour as the first */
+ p[i].c.r = r;
+ p[i].c.g = g;
+ p[i].c.b = b;
+ }
+ }
}
}
@@ -1720,9 +1768,6 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
kmeans);
break;
case 2:
- if (!strcmp(im->mode, "RGBA")) {
- withAlpha = 1;
- }
result = quantize_octree(
p,
im->xsize * im->ysize,
@@ -1734,9 +1779,6 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
break;
case 3:
#ifdef HAVE_LIBIMAGEQUANT
- if (!strcmp(im->mode, "RGBA")) {
- withAlpha = 1;
- }
result = quantize_pngquant(
p,
im->xsize,
diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c
index b8d4d1d7c..5e79bce35 100644
--- a/src/libImaging/QuantOctree.c
+++ b/src/libImaging/QuantOctree.c
@@ -317,7 +317,7 @@ void
add_lookup_buckets(ColorCube cube, ColorBucket palette, long nColors, long offset) {
long i;
Pixel p;
- for (i = offset; i < offset + nColors; i++) {
+ for (i = offset + nColors - 1; i >= offset; i--) {
avg_color_from_color_bucket(&palette[i], &p);
set_lookup_value(cube, &p, i);
}
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 021c2898c..38deb5360 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -56,7 +56,7 @@ _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) {
dump_state(state);
if (state->loc > state->eof) {
- TIFFError("_tiffReadProc", "Invalid Read at loc %llu, eof: %llu", state->loc, state->eof);
+ TIFFError("_tiffReadProc", "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, state->loc, state->eof);
return 0;
}
to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc);
@@ -181,7 +181,7 @@ _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) {
}
int
-ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) {
+ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) {
TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
TRACE(("initing libtiff\n"));
@@ -213,10 +213,10 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) {
}
int
-_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16 planarconfig, ImagingShuffler *unpackers) {
+_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16_t planarconfig, ImagingShuffler *unpackers) {
// if number of bands is 1, there is no difference with contig case
if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) {
- uint16 bits_per_sample = 8;
+ uint16_t bits_per_sample = 8;
TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bits_per_sample);
if (bits_per_sample != 8 && bits_per_sample != 16) {
@@ -265,7 +265,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) {
ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_block);
}
- if (ret != 1) {
+ if (ret != 1 || rows_per_block==(UINT32)(-1)) {
rows_per_block = state->ysize;
}
@@ -281,17 +281,6 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) {
img.req_orientation = ORIENTATION_TOPLEFT;
img.col_offset = 0;
- if (state->xsize != img.width || state->ysize != img.height) {
- TRACE(
- ("Inconsistent Image Error: %d =? %d, %d =? %d",
- state->xsize,
- img.width,
- state->ysize,
- img.height));
- state->errcode = IMAGING_CODEC_BROKEN;
- goto decodergba_err;
- }
-
/* overflow check for row byte size */
if (INT_MAX / 4 < img.width) {
state->errcode = IMAGING_CODEC_MEMORY;
@@ -427,15 +416,6 @@ _decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imaging
for (plane = 0; plane < planes; plane++) {
ImagingShuffler shuffler = unpackers[plane];
for (x = state->xoff; x < state->xsize; x += tile_width) {
- /* Sanity Check. Apparently in some cases, the TiffReadRGBA* functions
- have a different view of the size of the tiff than we're getting from
- other functions. So, we need to check here.
- */
- if (!TIFFCheckTile(tiff, x, y, 0, plane)) {
- TRACE(("Check Tile Error, Tile at %dx%d\n", x, y));
- state->errcode = IMAGING_CODEC_BROKEN;
- return -1;
- }
if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) {
TRACE(("Decode Error, Tile at %dx%d\n", x, y));
state->errcode = IMAGING_CODEC_BROKEN;
@@ -471,7 +451,7 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin
UINT8 *new_data;
UINT32 rows_per_strip;
int ret;
- tsize_t strip_size, row_byte_size;
+ tsize_t strip_size, row_byte_size, unpacker_row_byte_size;
ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip);
if (ret != 1 || rows_per_strip==(UINT32)(-1)) {
@@ -491,7 +471,8 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin
return -1;
}
- if (strip_size > ((state->xsize * state->bits / planes + 7) / 8) * rows_per_strip) {
+ unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8;
+ if (strip_size > (unpacker_row_byte_size * rows_per_strip)) {
// If the strip size as expected by LibTiff isn't what we're expecting, abort.
// man: TIFFStripSize returns the equivalent size for a strip of data as it would be returned in a
// call to TIFFReadEncodedStrip ...
@@ -505,7 +486,9 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin
row_byte_size = TIFFScanlineSize(tiff);
- if (row_byte_size == 0 || row_byte_size > strip_size) {
+ // if the unpacker calculated row size is > row byte size, (at least) the last
+ // row of the strip will have a read buffer overflow.
+ if (row_byte_size == 0 || unpacker_row_byte_size > row_byte_size) {
state->errcode = IMAGING_CODEC_BROKEN;
return -1;
}
@@ -560,14 +543,15 @@ ImagingLibTiffDecode(
Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes) {
TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
char *filename = "tempfile.tif";
- char *mode = "r";
+ char *mode = "rC";
TIFF *tiff;
- uint16 photometric = 0; // init to not PHOTOMETRIC_YCBCR
- uint16 compression;
+ uint16_t photometric = 0; // init to not PHOTOMETRIC_YCBCR
+ uint16_t compression;
int readAsRGBA = 0;
- uint16 planarconfig = 0;
+ uint16_t planarconfig = 0;
int planes = 1;
ImagingShuffler unpackers[4];
+ UINT32 img_width, img_height;
memset(unpackers, 0, sizeof(ImagingShuffler) * 4);
@@ -655,7 +639,7 @@ ImagingLibTiffDecode(
if (clientstate->ifd) {
int rv;
- uint32 ifdoffset = clientstate->ifd;
+ uint32_t ifdoffset = clientstate->ifd;
TRACE(("reading tiff ifd %u\n", ifdoffset));
rv = TIFFSetSubDirectory(tiff, ifdoffset);
if (!rv) {
@@ -664,6 +648,20 @@ ImagingLibTiffDecode(
}
}
+ TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &img_width);
+ TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &img_height);
+
+ if (state->xsize != img_width || state->ysize != img_height) {
+ TRACE(
+ ("Inconsistent Image Error: %d =? %d, %d =? %d",
+ state->xsize,
+ img_width,
+ state->ysize,
+ img_height));
+ state->errcode = IMAGING_CODEC_BROKEN;
+ goto decode_err;
+ }
+
TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric);
TIFFGetField(tiff, TIFFTAG_COMPRESSION, &compression);
@@ -674,7 +672,7 @@ ImagingLibTiffDecode(
readAsRGBA = photometric == PHOTOMETRIC_YCBCR;
if (readAsRGBA && compression == COMPRESSION_JPEG && planarconfig == PLANARCONFIG_CONTIG) {
- // If using new JPEG compression, let libjpeg do RGB convertion for performance reasons
+ // If using new JPEG compression, let libjpeg do RGB conversion for performance reasons
TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
readAsRGBA = 0;
}
@@ -699,8 +697,8 @@ ImagingLibTiffDecode(
// Check if raw mode was RGBa and it was stored on separate planes
// so we have to convert it to RGBA
if (planes > 3 && strcmp(im->mode, "RGBA") == 0) {
- uint16 extrasamples;
- uint16* sampleinfo;
+ uint16_t extrasamples;
+ uint16_t* sampleinfo;
ImagingShuffler shuffle;
INT32 y;
@@ -812,7 +810,7 @@ ImagingLibTiffMergeFieldInfo(
ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) {
// Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html)
TIFFSTATE *clientstate = (TIFFSTATE *)state->context;
- uint32 n;
+ uint32_t n;
int status = 0;
// custom fields added with ImagingLibTiffMergeFieldInfo are only used for
@@ -935,7 +933,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt
state->xsize);
if (TIFFWriteScanline(
- tiff, (tdata_t)(state->buffer), (uint32)state->y, 0) == -1) {
+ tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) {
TRACE(("Encode Error, row %d\n", state->y));
state->errcode = IMAGING_CODEC_BROKEN;
TIFFClose(tiff);
diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h
index 2c3d88caa..c7c7d48ed 100644
--- a/src/libImaging/TiffDecode.h
+++ b/src/libImaging/TiffDecode.h
@@ -32,17 +32,17 @@ typedef struct {
toff_t loc; /* toff_t == uint32 */
tsize_t size; /* tsize_t == int32 */
int fp;
- uint32 ifd; /* offset of the ifd, used for multipage
- * Should be uint32 for libtiff 3.9.x
- * uint64 for libtiff 4.0.x
- */
+ uint32_t ifd; /* offset of the ifd, used for multipage
+ * Should be uint32 for libtiff 3.9.x
+ * uint64 for libtiff 4.0.x
+ */
TIFF *tiff; /* Used in write */
toff_t eof;
int flrealloc; /* may we realloc */
} TIFFSTATE;
extern int
-ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset);
+ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset);
extern int
ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp);
extern int
diff --git a/src/outline.c b/src/outline.c
index ba3e056cc..0a9a3646e 100644
--- a/src/outline.c
+++ b/src/outline.c
@@ -145,11 +145,11 @@ _outline_transform(OutlineObject *self, PyObject *args) {
}
static struct PyMethodDef _outline_methods[] = {
- {"line", (PyCFunction)_outline_line, 1},
- {"curve", (PyCFunction)_outline_curve, 1},
- {"move", (PyCFunction)_outline_move, 1},
- {"close", (PyCFunction)_outline_close, 1},
- {"transform", (PyCFunction)_outline_transform, 1},
+ {"line", (PyCFunction)_outline_line, METH_VARARGS},
+ {"curve", (PyCFunction)_outline_curve, METH_VARARGS},
+ {"move", (PyCFunction)_outline_move, METH_VARARGS},
+ {"close", (PyCFunction)_outline_close, METH_VARARGS},
+ {"transform", (PyCFunction)_outline_transform, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
diff --git a/src/path.c b/src/path.c
index 8d1f68e84..dea274ee3 100644
--- a/src/path.c
+++ b/src/path.c
@@ -57,7 +57,7 @@ alloc_array(Py_ssize_t count) {
if ((unsigned long long)count > (SIZE_MAX / (2 * sizeof(double))) - 1) {
return ImagingError_MemoryError();
}
- xy = malloc(2 * count * sizeof(double) + 1);
+ xy = calloc(2 * count * sizeof(double) + 1, sizeof(double));
if (!xy) {
ImagingError_MemoryError();
}
@@ -327,21 +327,26 @@ path_getbbox(PyPathObject *self, PyObject *args) {
xy = self->xy;
- x0 = x1 = xy[0];
- y0 = y1 = xy[1];
+ if (self->count == 0) {
+ x0 = x1 = 0;
+ y0 = y1 = 0;
+ } else {
+ x0 = x1 = xy[0];
+ y0 = y1 = xy[1];
- for (i = 1; i < self->count; i++) {
- if (xy[i + i] < x0) {
- x0 = xy[i + i];
- }
- if (xy[i + i] > x1) {
- x1 = xy[i + i];
- }
- if (xy[i + i + 1] < y0) {
- y0 = xy[i + i + 1];
- }
- if (xy[i + i + 1] > y1) {
- y1 = xy[i + i + 1];
+ for (i = 1; i < self->count; i++) {
+ if (xy[i + i] < x0) {
+ x0 = xy[i + i];
+ }
+ if (xy[i + i] > x1) {
+ x1 = xy[i + i];
+ }
+ if (xy[i + i + 1] < y0) {
+ y0 = xy[i + i + 1];
+ }
+ if (xy[i + i + 1] > y1) {
+ y1 = xy[i + i + 1];
+ }
}
}
@@ -524,11 +529,11 @@ path_transform(PyPathObject *self, PyObject *args) {
}
static struct PyMethodDef methods[] = {
- {"getbbox", (PyCFunction)path_getbbox, 1},
- {"tolist", (PyCFunction)path_tolist, 1},
- {"compact", (PyCFunction)path_compact, 1},
- {"map", (PyCFunction)path_map, 1},
- {"transform", (PyCFunction)path_transform, 1},
+ {"getbbox", (PyCFunction)path_getbbox, METH_VARARGS},
+ {"tolist", (PyCFunction)path_tolist, METH_VARARGS},
+ {"compact", (PyCFunction)path_compact, METH_VARARGS},
+ {"map", (PyCFunction)path_map, METH_VARARGS},
+ {"transform", (PyCFunction)path_transform, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
diff --git a/src/thirdparty/fribidi-shim/fribidi.c b/src/thirdparty/fribidi-shim/fribidi.c
index 55e2a6ab3..76fd5b9a4 100644
--- a/src/thirdparty/fribidi-shim/fribidi.c
+++ b/src/thirdparty/fribidi-shim/fribidi.c
@@ -12,7 +12,7 @@
/* FriBiDi>=1.0.0 adds bracket_types param, ignore and call legacy function */
-FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
+static FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
const FriBidiCharType *bidi_types,
const FriBidiBracketType *bracket_types,
const FriBidiStrIndex len,
@@ -24,7 +24,7 @@ FriBidiLevel fribidi_get_par_embedding_levels_ex_compat(
}
/* FriBiDi>=1.0.0 gets bracket types here, ignore */
-void fribidi_get_bracket_types_compat(
+static void fribidi_get_bracket_types_compat(
const FriBidiChar *str,
const FriBidiStrIndex len,
const FriBidiCharType *types,
@@ -56,6 +56,9 @@ int load_fribidi(void) {
error = error || (func == 0);
p_fribidi = LoadLibrary("fribidi");
+ if (!p_fribidi) {
+ p_fribidi = LoadLibrary("fribidi-0");
+ }
/* MSYS2 */
if (!p_fribidi) {
p_fribidi = LoadLibrary("libfribidi-0");
diff --git a/src/thirdparty/fribidi-shim/fribidi.h b/src/thirdparty/fribidi-shim/fribidi.h
index 7712a5b22..7e175c3db 100644
--- a/src/thirdparty/fribidi-shim/fribidi.h
+++ b/src/thirdparty/fribidi-shim/fribidi.h
@@ -63,8 +63,12 @@ typedef uint32_t FriBidiParType;
/* functions */
#ifdef FRIBIDI_SHIM_IMPLEMENTATION
+#ifdef _MSC_VER
#define FRIBIDI_ENTRY
#else
+#define FRIBIDI_ENTRY __attribute__((visibility ("hidden")))
+#endif
+#else
#define FRIBIDI_ENTRY extern
#endif
diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS
index 29c9ae0e5..c49176a95 100644
--- a/src/thirdparty/raqm/NEWS
+++ b/src/thirdparty/raqm/NEWS
@@ -1,3 +1,17 @@
+Overview of changes leading to 0.7.1
+Monday, September 27, 2021
+====================================
+
+Fix test failure with newer HarfBuzz versions.
+
+Apply FT_Face transformation matrix when built against FreeType 2.11 or later.
+
+Add meson build system. Autotools build system will be dropped in next release.
+
+Improve MSVC support.
+
+Build and documentation fixes.
+
Overview of changes leading to 0.7.1
Sunday, November 22, 2020
====================================
diff --git a/src/thirdparty/raqm/README b/src/thirdparty/raqm/README.md
similarity index 79%
rename from src/thirdparty/raqm/README
rename to src/thirdparty/raqm/README.md
index 7940bf3b6..64937343a 100644
--- a/src/thirdparty/raqm/README
+++ b/src/thirdparty/raqm/README.md
@@ -1,8 +1,7 @@
Raqm
====
-[](https://travis-ci.org/HOST-Oman/libraqm)
-[](https://ci.appveyor.com/project/HOSTOman/libraqm)
+[](https://github.com/HOST-Oman/libraqm/actions)
Raqm is a small library that encapsulates the logic for complex text layout and
provides a convenient API.
@@ -15,7 +14,7 @@ The documentation can be accessed on the web at:
> http://host-oman.github.io/libraqm/
Raqm (Arabic: رَقْم) is writing, also number or digit and the Arabic word for
-digital (رَقَمِيّ) shares the same root, so it is a play on “digital writing”.
+digital (رَقَمِيّ) shares the same root, so it is a play on “digital writing”.
Building
--------
@@ -30,31 +29,30 @@ To build the documentation you will also need:
To install dependencies on Fedora:
- sudo dnf install freetype-devel harfbuzz-devel fribidi-devel gtk-doc
+ sudo dnf install freetype-devel harfbuzz-devel fribidi-devel meson gtk-doc
To install dependencies on Ubuntu:
- sudo apt-get install libfreetype6-dev libharfbuzz-dev libfribidi-dev \
- gtk-doc-tools
+ sudo apt-get install libfreetype6-dev libharfbuzz-dev libfribidi-dev meson gtk-doc-tools
On Mac OS X you can use Homebrew:
- brew install freetype harfbuzz fribidi gtk-doc
+ brew install freetype harfbuzz fribidi meson gtk-doc
export XML_CATALOG_FILES="/usr/local/etc/xml/catalog" # for the docs
Once you have the source code and the dependencies, you can proceed to build.
To do that, run the customary sequence of commands in the source code
directory:
- $ ./configure
- $ make
- $ make install
+ $ meson build
+ $ ninja -C build
+ $ ninja -C build install
-To build the documentation, pass `--enable-gtk-doc` to the `configure` script.
+To build the documentation, pass `-Ddocs=enable` to the `meson`.
To run the tests:
- $ make check
+ $ ninja -C test
Contributing
------------
diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h
index 94b25ada7..8b115f612 100644
--- a/src/thirdparty/raqm/raqm-version.h
+++ b/src/thirdparty/raqm/raqm-version.h
@@ -33,9 +33,9 @@
#define RAQM_VERSION_MAJOR 0
#define RAQM_VERSION_MINOR 7
-#define RAQM_VERSION_MICRO 1
+#define RAQM_VERSION_MICRO 2
-#define RAQM_VERSION_STRING "0.7.1"
+#define RAQM_VERSION_STRING "0.7.2"
#define RAQM_VERSION_ATLEAST(major,minor,micro) \
((major)*10000+(minor)*100+(micro) <= \
diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c
index 5a0b2078e..31161c9d9 100644
--- a/src/thirdparty/raqm/raqm.c
+++ b/src/thirdparty/raqm/raqm.c
@@ -39,6 +39,21 @@
#include
#include
+#if FREETYPE_MAJOR > 2 || \
+ FREETYPE_MAJOR == 2 && FREETYPE_MINOR >= 11
+#define HAVE_FT_GET_TRANSFORM
+#endif
+
+#if HB_VERSION_ATLEAST(2, 0, 0)
+#define HAVE_HB_BUFFER_SET_INVISIBLE_GLYPH
+#endif
+
+#if HB_VERSION_ATLEAST(1, 8, 0)
+#define HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES 1
+#else
+#define HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES 0
+#endif
+
#include "raqm.h"
#if FRIBIDI_MAJOR_VERSION >= 1
@@ -455,8 +470,6 @@ raqm_set_text_utf8 (raqm_t *rq,
return true;
}
- RAQM_TEST ("Text is: %s\n", text);
-
rq->flags |= RAQM_FLAG_UTF8;
rq->text_utf8 = malloc (sizeof (char) * len);
@@ -491,7 +504,7 @@ raqm_set_text_utf8 (raqm_t *rq,
*
* The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph
* direction based on the first character with strong bidi type (see [rule
- * P2](http://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
+ * P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
* which can be good enough for many cases but has problems when a mainly
* right-to-left paragraph starts with a left-to-right character and vice versa
* as the detected paragraph direction will be the wrong one, or when text does
@@ -1556,6 +1569,21 @@ _raqm_resolve_scripts (raqm_t *rq)
return true;
}
+static void
+_raqm_ft_transform (int *x,
+ int *y,
+ FT_Matrix matrix)
+{
+ FT_Vector vector;
+ vector.x = *x;
+ vector.y = *y;
+
+ FT_Vector_Transform (&vector, &matrix);
+
+ *x = vector.x;
+ *y = vector.y;
+}
+
static bool
_raqm_shape (raqm_t *rq)
{
@@ -1585,6 +1613,22 @@ _raqm_shape (raqm_t *rq)
hb_shape_full (run->font, run->buffer, rq->features, rq->features_len,
NULL);
+
+#ifdef HAVE_FT_GET_TRANSFORM
+ {
+ FT_Matrix matrix;
+ hb_glyph_position_t *pos;
+ unsigned int len;
+
+ FT_Get_Transform (hb_ft_font_get_face (run->font), &matrix, NULL);
+ pos = hb_buffer_get_glyph_positions (run->buffer, &len);
+ for (unsigned int i = 0; i < len; i++)
+ {
+ _raqm_ft_transform (&pos[i].x_advance, &pos[i].y_advance, matrix);
+ _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix);
+ }
+ }
+#endif
}
return true;
diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h
index 1a33fe8ba..342afc8b2 100644
--- a/src/thirdparty/raqm/raqm.h
+++ b/src/thirdparty/raqm/raqm.h
@@ -30,6 +30,10 @@
#include "config.h"
#endif
+#ifndef RAQM_API
+#define RAQM_API
+#endif
+
#include
#include
#include
@@ -93,86 +97,86 @@ typedef struct raqm_glyph_t {
FT_Face ftface;
} raqm_glyph_t;
-raqm_t *
+RAQM_API raqm_t *
raqm_create (void);
-raqm_t *
+RAQM_API raqm_t *
raqm_reference (raqm_t *rq);
-void
+RAQM_API void
raqm_destroy (raqm_t *rq);
-bool
+RAQM_API bool
raqm_set_text (raqm_t *rq,
const uint32_t *text,
size_t len);
-bool
+RAQM_API bool
raqm_set_text_utf8 (raqm_t *rq,
const char *text,
size_t len);
-bool
+RAQM_API bool
raqm_set_par_direction (raqm_t *rq,
raqm_direction_t dir);
-bool
+RAQM_API bool
raqm_set_language (raqm_t *rq,
const char *lang,
size_t start,
size_t len);
-bool
+RAQM_API bool
raqm_add_font_feature (raqm_t *rq,
const char *feature,
int len);
-bool
+RAQM_API bool
raqm_set_freetype_face (raqm_t *rq,
FT_Face face);
-bool
+RAQM_API bool
raqm_set_freetype_face_range (raqm_t *rq,
FT_Face face,
size_t start,
size_t len);
-bool
+RAQM_API bool
raqm_set_freetype_load_flags (raqm_t *rq,
int flags);
-bool
+RAQM_API bool
raqm_set_invisible_glyph (raqm_t *rq,
int gid);
-bool
+RAQM_API bool
raqm_layout (raqm_t *rq);
-raqm_glyph_t *
+RAQM_API raqm_glyph_t *
raqm_get_glyphs (raqm_t *rq,
size_t *length);
-bool
+RAQM_API bool
raqm_index_to_position (raqm_t *rq,
size_t *index,
int *x,
int *y);
-bool
+RAQM_API bool
raqm_position_to_index (raqm_t *rq,
int x,
int y,
size_t *index);
-void
+RAQM_API void
raqm_version (unsigned int *major,
unsigned int *minor,
unsigned int *micro);
-const char *
+RAQM_API const char *
raqm_version_string (void);
-bool
+RAQM_API bool
raqm_version_atleast (unsigned int major,
unsigned int minor,
unsigned int micro);
diff --git a/tox.ini b/tox.ini
index 2557d5067..bdedc2bd5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,13 +6,13 @@
[tox]
envlist =
lint
- py{36,37,38,39,py3}
+ py{37,38,39,310,py3}
minversion = 1.9
[testenv]
commands =
- {envpython} setup.py clean
- {envpython} setup.py build_ext --inplace
+ make clean
+ {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" .
{envpython} selftest.py
{envpython} -m pytest -W always {posargs}
deps =
diff --git a/winbuild/build.rst b/winbuild/build.rst
index cd4a45e87..b30a94226 100644
--- a/winbuild/build.rst
+++ b/winbuild/build.rst
@@ -55,8 +55,8 @@ behaviour of ``build_prepare.py``:
* ``-v`` will print generated scripts.
* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency
-* ``--no-raqm`` will skip optional dependency Raqm (which itself depends on
- LGPL-licensed ``fribidi``).
+* ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi
+ (required for Raqm text shaping).
* ``--python=`` and ``--executable=`` override ``PYTHON`` and ``EXECUTABLE``.
* ``--architecture=`` overrides ``ARCHITECTURE``.
* ``--dir=`` and ``--depends=`` override ``PILLOW_BUILD``
@@ -87,7 +87,7 @@ and install Pillow in develop mode (instead of ``python3 -m pip install --editab
Testing Pillow
--------------
-Some binary dependencies (e.g. ``libraqm.dll``) will be stored in the
+Some binary dependencies (e.g. ``fribidi.dll``) will be stored in the
``winbuild\build\bin`` directory; this directory should be added to ``PATH``
before running tests.
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index a91c86a73..0589baf21 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -105,9 +105,9 @@ header = [
# dependencies, listed in order of compilation
deps = {
"libjpeg": {
- "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.6/libjpeg-turbo-2.0.6.tar.gz",
- "filename": "libjpeg-turbo-2.0.6.tar.gz",
- "dir": "libjpeg-turbo-2.0.6",
+ "url": SF_MIRROR + "/project/libjpeg-turbo/2.1.2/libjpeg-turbo-2.1.2.tar.gz",
+ "filename": "libjpeg-turbo-2.1.2.tar.gz",
+ "dir": "libjpeg-turbo-2.1.2",
"build": [
cmd_cmake(
[
@@ -141,22 +141,22 @@ deps = {
"libs": [r"*.lib"],
},
"libtiff": {
- "url": "https://download.osgeo.org/libtiff/tiff-4.2.0.tar.gz",
- "filename": "tiff-4.2.0.tar.gz",
- "dir": "tiff-4.2.0",
+ "url": "https://download.osgeo.org/libtiff/tiff-4.3.0.tar.gz",
+ "filename": "tiff-4.3.0.tar.gz",
+ "dir": "tiff-4.3.0",
"build": [
- cmd_copy(r"{winbuild_dir}\tiff.opt", "nmake.opt"),
- cmd_nmake("makefile.vc", "clean"),
- cmd_nmake("makefile.vc", "lib"),
+ cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"),
+ cmd_nmake(target="clean"),
+ cmd_nmake(target="tiff"),
],
"headers": [r"libtiff\tiff*.h"],
"libs": [r"libtiff\*.lib"],
# "bins": [r"libtiff\*.dll"],
},
"libwebp": {
- "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.0.tar.gz",
- "filename": "libwebp-1.2.0.tar.gz",
- "dir": "libwebp-1.2.0",
+ "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.1.tar.gz",
+ "filename": "libwebp-1.2.1.tar.gz",
+ "dir": "libwebp-1.2.1",
"build": [
cmd_rmdir(r"output\release-static"), # clean
cmd_nmake(
@@ -184,9 +184,9 @@ deps = {
"libs": [r"libpng16.lib"],
},
"freetype": {
- "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.4.tar.gz", # noqa: E501
- "filename": "freetype-2.10.4.tar.gz",
- "dir": "freetype-2.10.4",
+ "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.11.1.tar.gz", # noqa: E501
+ "filename": "freetype-2.11.1.tar.gz",
+ "dir": "freetype-2.11.1",
"patch": {
r"builds\windows\vc2010\freetype.vcxproj": {
# freetype setting is /MD for .dll and /MT for .lib, we need /MD
@@ -236,7 +236,9 @@ deps = {
cmd_rmdir("Lib"),
cmd_rmdir(r"Projects\VC2017\Release"),
cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "Clean"),
- cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static"),
+ cmd_msbuild(
+ r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static:Rebuild"
+ ),
cmd_xcopy("include", "{inc_dir}"),
],
"libs": [r"Lib\MS\*.lib"],
@@ -255,29 +257,30 @@ deps = {
"libs": [r"bin\*.lib"],
},
"libimagequant": {
- # Merge master into msvc (matches 2.14.1 except for version bump)
- "url": "https://github.com/ImageOptim/libimagequant/archive/16adaded22d1f90db5c9154a06d00a8b672ca09a.zip", # noqa: E501
- "filename": "libimagequant-16adaded22d1f90db5c9154a06d00a8b672ca09a.zip",
- "dir": "libimagequant-16adaded22d1f90db5c9154a06d00a8b672ca09a",
+ # commit: Merge branch 'master' into msvc (matches 2.17.0 tag)
+ "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501
+ "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
+ "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab",
"patch": {
"CMakeLists.txt": {
- "add_library": "add_compile_options(-openmp-)\r\nadd_library",
- " SHARED": " STATIC",
+ "if(OPENMP_FOUND)": "if(false)",
+ "install": "#install",
}
},
"build": [
# lint: do not inline
cmd_cmake(),
cmd_nmake(target="clean"),
- cmd_nmake(),
+ cmd_nmake(target="imagequant_a"),
+ cmd_copy("imagequant_a.lib", "imagequant.lib"),
],
"headers": [r"*.h"],
- "libs": [r"*.lib"],
+ "libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/2.8.0.zip",
- "filename": "harfbuzz-2.8.0.zip",
- "dir": "harfbuzz-2.8.0",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/3.2.0.zip",
+ "filename": "harfbuzz-3.2.0.zip",
+ "dir": "harfbuzz-3.2.0",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),
@@ -287,9 +290,9 @@ deps = {
"libs": [r"*.lib"],
},
"fribidi": {
- "url": "https://github.com/fribidi/fribidi/archive/v1.0.10.zip",
- "filename": "fribidi-1.0.10.zip",
- "dir": "fribidi-1.0.10",
+ "url": "https://github.com/fribidi/fribidi/archive/v1.0.11.zip",
+ "filename": "fribidi-1.0.11.zip",
+ "dir": "fribidi-1.0.11",
"build": [
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
cmd_cmake(),
@@ -435,6 +438,7 @@ def build_dep(name):
assert patch_from in text
text = text.replace(patch_from, patch_to)
with open(patch_file, "w") as f:
+ print(f"Patching {patch_file}")
f.write(text)
banner = f"Building {name} ({dir})"
@@ -470,8 +474,6 @@ def build_pillow():
cmd_cd("{pillow_dir}"),
*prefs["header"],
cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow
- cmd_set("MSSdk", "1"), # for PyPy3.6
- cmd_set("py_vcruntime_redist", "true"), # use /MD, not /MT
r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501
]
diff --git a/winbuild/raqm.cmake b/winbuild/raqm.cmake
deleted file mode 100644
index 82c9cdc70..000000000
--- a/winbuild/raqm.cmake
+++ /dev/null
@@ -1,39 +0,0 @@
-cmake_minimum_required(VERSION 3.12)
-
-project(libraqm)
-
-
-find_library(fribidi NAMES fribidi)
-find_library(harfbuzz NAMES harfbuzz)
-find_library(freetype NAMES freetype)
-
-add_definitions(-DFRIBIDI_LIB_STATIC)
-
-
-function(raqm_conf)
- file(READ configure.ac RAQM_CONF)
- string(REGEX MATCH "\\[([0-9]+)\\.([0-9]+)\\.([0-9]+)\\]," _ "${RAQM_CONF}")
- set(RAQM_VERSION_MAJOR "${CMAKE_MATCH_1}")
- set(RAQM_VERSION_MINOR "${CMAKE_MATCH_2}")
- set(RAQM_VERSION_MICRO "${CMAKE_MATCH_3}")
- set(RAQM_VERSION "${RAQM_VERSION_MAJOR}.${RAQM_VERSION_MINOR}.${RAQM_VERSION_MICRO}")
- message("detected libraqm version ${RAQM_VERSION}")
- configure_file(src/raqm-version.h.in src/raqm-version.h @ONLY)
-endfunction()
-raqm_conf()
-
-
-set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
-set(RAQM_SOURCES
- src/raqm.c)
-set(RAQM_HEADERS
- src/raqm.h
- src/raqm-version.h)
-
-add_library(libraqm SHARED
- ${RAQM_SOURCES}
- ${RAQM_HEADERS})
-target_link_libraries(libraqm
- ${fribidi}
- ${harfbuzz}
- ${freetype})
diff --git a/winbuild/tiff.opt b/winbuild/tiff.opt
deleted file mode 100644
index d82c51678..000000000
--- a/winbuild/tiff.opt
+++ /dev/null
@@ -1,220 +0,0 @@
-# $Id: nmake.opt,v 1.18 2006/06/07 16:33:45 dron Exp $
-#
-# Copyright (C) 2004, Andrey Kiselev
-#
-# Permission to use, copy, modify, distribute, and sell this software and
-# its documentation for any purpose is hereby granted without fee, provided
-# that (i) the above copyright notices and this permission notice appear in
-# all copies of the software and related documentation, and (ii) the names of
-# Sam Leffler and Silicon Graphics may not be used in any advertising or
-# publicity relating to the software without the specific, prior written
-# permission of Sam Leffler and Silicon Graphics.
-#
-# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
-# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
-#
-# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
-# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
-# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
-# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
-# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
-# OF THIS SOFTWARE.
-
-# Compile time parameters for MS Visual C++ compiler.
-# You may edit this file to specify building options.
-
-#
-###### Edit the following lines to choose a feature set you need. #######
-#
-
-#
-# Select WINMODE_CONSOLE to build a library which reports errors to stderr, or
-# WINMODE_WINDOWED to build such that errors are reported via MessageBox().
-#
-WINMODE_CONSOLE = 1
-#WINMODE_WINDOWED = 1
-
-#
-# Comment out the following lines to disable internal codecs.
-#
-# Support for CCITT Group 3 & 4 algorithms
-CCITT_SUPPORT = 1
-# Support for Macintosh PackBits algorithm
-PACKBITS_SUPPORT = 1
-# Support for LZW algorithm
-LZW_SUPPORT = 1
-# Support for ThunderScan 4-bit RLE algorithm
-THUNDER_SUPPORT = 1
-# Support for NeXT 2-bit RLE algorithm
-NEXT_SUPPORT = 1
-# Support for LogLuv high dynamic range encoding
-LOGLUV_SUPPORT = 1
-
-#
-# Uncomment and edit following lines to enable JPEG support.
-#
-JPEG_SUPPORT = 1
-JPEG_INCLUDE = -I$(INCLIB)
-JPEG_LIB = $(INCLIB)/libjpeg.lib
-
-#
-# Uncomment and edit following lines to enable ZIP support
-# (required for Deflate compression and Pixar log-format)
-#
-ZIP_SUPPORT = 1
-ZLIB_INCLUDE = -I$(INCLIB)
-ZLIB_LIB = $(INCLIB)/zlib.lib
-
-# Indicate if the compiler provides strtoll/strtoull (default 1)
-# Users of MSVC++ 14.0 ("Visual Studio 2015") and later should set this to 1
-HAVE_STRTOLL = 1
-
-#
-# Uncomment and edit following lines to enable ISO JBIG support
-#
-#JBIG_SUPPORT = 1
-#JBIGDIR = d:/projects/jbigkit
-#JBIG_INCLUDE = -I$(JBIGDIR)/libjbig
-#JBIG_LIB = $(JBIGDIR)/libjbig/jbig.lib
-
-#
-# Uncomment following line to enable Pixar log-format algorithm
-# (Zlib required).
-#
-#PIXARLOG_SUPPORT = 1
-
-#
-# Comment out the following lines to disable strip chopping
-# (whether or not to convert single-strip uncompressed images to multiple
-# strips of specified size to reduce memory usage). Default strip size
-# is 8192 bytes, it can be configured via the STRIP_SIZE_DEFAULT parameter
-#
-STRIPCHOP_SUPPORT = 1
-STRIP_SIZE_DEFAULT = 8192
-
-#
-# Comment out the following lines to disable treating the fourth sample with
-# no EXTRASAMPLE_ value as being ASSOCALPHA. Many packages produce RGBA
-# files but don't mark the alpha properly.
-#
-EXTRASAMPLE_AS_ALPHA_SUPPORT = 1
-
-#
-# Comment out the following lines to disable picking up YCbCr subsampling
-# info from the JPEG data stream to support files lacking the tag.
-# See Bug 168 in Bugzilla, and JPEGFixupTestSubsampling() for details.
-#
-CHECK_JPEG_YCBCR_SUBSAMPLING = 1
-
-#
-####################### Compiler related options. #######################
-#
-
-#
-# Pick debug or optimized build flags. We default to an optimized build
-# with no debugging information.
-# NOTE: /EHsc option required if you want to build the C++ stream API
-#
-OPTFLAGS = /Ox /MD /EHsc /W3 /D_CRT_SECURE_NO_DEPRECATE
-#OPTFLAGS = /Zi
-
-#
-# Uncomment following line to enable using Windows Common RunTime Library
-# instead of Windows specific system calls. See notes on top of tif_unix.c
-# module for details.
-#
-USE_WIN_CRT_LIB = 1
-
-# Compiler specific options. You may probably want to adjust compilation
-# parameters in CFLAGS variable. Refer to your compiler documentation
-# for the option reference.
-#
-MAKE = nmake /nologo
-CC = cl /nologo
-CXX = cl /nologo
-AR = lib /nologo
-LD = link /nologo
-
-CFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS)
-CXXFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS)
-EXTRAFLAGS =
-LIBS =
-
-# Name of the output shared library
-DLLNAME = libtiff.dll
-
-#
-########### There is nothing to edit below this line normally. ###########
-#
-
-# Set the native cpu bit order
-EXTRAFLAGS = -DFILLODER_LSB2MSB $(EXTRAFLAGS)
-
-!IFDEF WINMODE_WINDOWED
-EXTRAFLAGS = -DTIF_PLATFORM_WINDOWED $(EXTRAFLAGS)
-LIBS = user32.lib $(LIBS)
-!ELSE
-EXTRAFLAGS = -DTIF_PLATFORM_CONSOLE $(EXTRAFLAGS)
-!ENDIF
-
-# Codec stuff
-!IFDEF CCITT_SUPPORT
-EXTRAFLAGS = -DCCITT_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF PACKBITS_SUPPORT
-EXTRAFLAGS = -DPACKBITS_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF LZW_SUPPORT
-EXTRAFLAGS = -DLZW_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF THUNDER_SUPPORT
-EXTRAFLAGS = -DTHUNDER_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF NEXT_SUPPORT
-EXTRAFLAGS = -DNEXT_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF LOGLUV_SUPPORT
-EXTRAFLAGS = -DLOGLUV_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF JPEG_SUPPORT
-LIBS = $(LIBS) $(JPEG_LIB)
-EXTRAFLAGS = -DJPEG_SUPPORT -DOJPEG_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF ZIP_SUPPORT
-LIBS = $(LIBS) $(ZLIB_LIB)
-EXTRAFLAGS = -DZIP_SUPPORT $(EXTRAFLAGS)
-!IFDEF PIXARLOG_SUPPORT
-EXTRAFLAGS = -DPIXARLOG_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-!ENDIF
-
-!IFDEF JBIG_SUPPORT
-LIBS = $(LIBS) $(JBIG_LIB)
-EXTRAFLAGS = -DJBIG_SUPPORT $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF STRIPCHOP_SUPPORT
-EXTRAFLAGS = -DSTRIPCHOP_DEFAULT=TIFF_STRIPCHOP -DSTRIP_SIZE_DEFAULT=$(STRIP_SIZE_DEFAULT) $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF EXTRASAMPLE_AS_ALPHA_SUPPORT
-EXTRAFLAGS = -DDEFAULT_EXTRASAMPLE_AS_ALPHA $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF CHECK_JPEG_YCBCR_SUBSAMPLING
-EXTRAFLAGS = -DCHECK_JPEG_YCBCR_SUBSAMPLING $(EXTRAFLAGS)
-!ENDIF
-
-!IFDEF USE_WIN_CRT_LIB
-EXTRAFLAGS = -DAVOID_WIN32_FILEIO $(EXTRAFLAGS)
-!ELSE
-EXTRAFLAGS = -DUSE_WIN32_FILEIO $(EXTRAFLAGS)
-!ENDIF