diff --git a/.appveyor.yml b/.appveyor.yml
index 20908052b..b817cd9d8 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -10,7 +10,7 @@ environment:
TEST_OPTIONS:
DEPLOY: YES
matrix:
- - PYTHON: C:/Python310
+ - PYTHON: C:/Python311
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python37-x64
diff --git a/.editorconfig b/.editorconfig
index d74549fe2..449530717 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,6 +13,10 @@ indent_style = space
trim_trailing_whitespace = true
+[*.rst]
+# Four-space indentation
+indent_size = 4
+
[*.yml]
# Two-space indentation
indent_size = 2
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6195f973b..49611e287 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -30,7 +30,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: "3.10"
+ python-version: "3.x"
cache: pip
cache-dependency-path: "setup.py"
diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh
index 65f2b81d5..dfd7d0553 100755
--- a/.github/workflows/macos-install.sh
+++ b/.github/workflows/macos-install.sh
@@ -2,7 +2,7 @@
set -e
-brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm
+brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage
@@ -13,7 +13,6 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
-echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
python3 -m pip install numpy
# extra test images
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index ffac91cec..8c210bc90 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: "Check issues"
- uses: actions/stale@v6
+ uses: actions/stale@v7
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action"
diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml
index 5b9ab0eda..7b8070d34 100644
--- a/.github/workflows/test-cygwin.yml
+++ b/.github/workflows/test-cygwin.yml
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-minor-version: [7, 8, 9]
+ python-minor-version: [8, 9]
timeout-minutes: 40
@@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v3
- name: Install Cygwin
- uses: cygwin/cygwin-install-action@v2
+ uses: cygwin/cygwin-install-action@v3
with:
platform: x86_64
packages: >
@@ -48,7 +48,7 @@ jobs:
qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH
- uses: egor-tensin/cleanup-path@v2
+ uses: egor-tensin/cleanup-path@v3
with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
@@ -76,7 +76,7 @@ jobs:
- name: Build
shell: bash.exe -eo pipefail -o igncr "{0}"
run: |
- .ci/build.sh
+ SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh
- name: Test
run: |
diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml
index c68d43935..7331cf8ee 100644
--- a/.github/workflows/test-docker.yml
+++ b/.github/workflows/test-docker.yml
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -30,8 +30,8 @@ jobs:
centos-stream-9-amd64,
debian-10-buster-x86,
debian-11-bullseye-x86,
- fedora-35-amd64,
fedora-36-amd64,
+ fedora-37-amd64,
gentoo,
ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64,
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 36bd03e7e..487c3586f 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -15,13 +15,13 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
+ python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
architecture: ["x86", "x64"]
include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows
- - python-version: "pypy-3.7"
+ - python-version: "pypy3.8"
architecture: "x64"
- - python-version: "pypy-3.8"
+ - python-version: "pypy3.9"
architecture: "x64"
timeout-minutes: 30
@@ -65,7 +65,9 @@ jobs:
xcopy /S /Y winbuild\depends\test_images\* Tests\images\
# make cache key depend on VS version
- & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" | find """catalog_buildVersion""" | ForEach-Object { $a = $_.split(" ")[1]; echo "::set-output name=vs::$a" }
+ & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" `
+ | find """catalog_buildVersion""" `
+ | ForEach-Object { $a = $_.split(" ")[1]; echo "vs=$a" >> $env:GITHUB_OUTPUT }
shell: pwsh
- name: Cache build
@@ -90,19 +92,28 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_zlib.cmd"
- - name: Build dependencies / LibTiff
+ - name: Build dependencies / xz
if: steps.build-cache.outputs.cache-hit != 'true'
- run: "& winbuild\\build\\build_dep_libtiff.cmd"
+ run: "& winbuild\\build\\build_dep_xz.cmd"
- name: Build dependencies / WebP
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libwebp.cmd"
+ - name: Build dependencies / LibTiff
+ if: steps.build-cache.outputs.cache-hit != 'true'
+ run: "& winbuild\\build\\build_dep_libtiff.cmd"
+
# for FreeType CBDT/SBIX font support
- name: Build dependencies / libpng
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libpng.cmd"
+ # for FreeType WOFF2 font support
+ - name: Build dependencies / brotli
+ if: steps.build-cache.outputs.cache-hit != 'true'
+ run: "& winbuild\\build\\build_dep_brotli.cmd"
+
- name: Build dependencies / FreeType
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_freetype.cmd"
@@ -130,7 +141,7 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_fribidi.cmd"
- # trim ~150MB x 9
+ # trim ~150MB for each job
- name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true'
run: rmdir /S /Q winbuild\build\src
@@ -185,16 +196,42 @@ jobs:
id: wheel
if: "github.event_name != 'pull_request'"
run: |
- for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a
+ mkdir fribidi\${{ matrix.architecture }}
+ copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }}
+ setlocal EnableDelayedExpansion
+ for %%f in (winbuild\build\license\*) do (
+ set x=%%~nf
+ rem Skip FriBiDi license, it is not included in the wheel.
+ set fribidi=!x:~0,7!
+ if NOT !fribidi!==fribidi (
+ rem Skip imagequant license, it is not included in the wheel.
+ set libimagequant=!x:~0,13!
+ if NOT !libimagequant!==libimagequant (
+ echo. >> LICENSE
+ echo ===== %%~nf ===== >> LICENSE
+ echo. >> LICENSE
+ type %%f >> LICENSE
+ )
+ )
+ )
+ for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd
- - uses: actions/upload-artifact@v3
+ - name: Upload wheel
+ uses: actions/upload-artifact@v3
if: "github.event_name != 'pull_request'"
with:
name: ${{ steps.wheel.outputs.dist }}
path: dist\*.whl
+ - name: Upload fribidi.dll
+ if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
+ uses: actions/upload-artifact@v3
+ with:
+ name: fribidi
+ path: fribidi\*
+
success:
permissions:
contents: none
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4c8a1b85f..11c7b77be 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
-concurrency:
+concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -20,9 +20,9 @@ jobs:
"ubuntu-latest",
]
python-version: [
- "pypy-3.8",
- "pypy-3.7",
- "3.11-dev",
+ "pypy3.9",
+ "pypy3.8",
+ "3.11",
"3.10",
"3.9",
"3.8",
@@ -96,7 +96,7 @@ jobs:
path: Tests/errors
- name: Docs
- if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
+ if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11
run: |
make doccheck
diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml
deleted file mode 100644
index 69f9e5476..000000000
--- a/.github/workflows/tidelift.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Tidelift Align
-
-on:
- schedule:
- - cron: "30 2 * * *" # daily at 02:30 UTC
- push:
- paths:
- - "Pipfile*"
- - ".github/workflows/tidelift.yml"
- pull_request:
- paths:
- - "Pipfile*"
- - ".github/workflows/tidelift.yml"
- workflow_dispatch:
-
-permissions:
- contents: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-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@v3
- - 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 f81bcb956..d019d3e7f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,18 +1,25 @@
repos:
- repo: https://github.com/psf/black
- rev: 22.8.0
+ rev: 22.12.0
hooks:
- id: black
- args: ["--target-version", "py37"]
+ 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: 5.10.1
+ rev: 5.11.1
hooks:
- id: isort
+ - repo: https://github.com/PyCQA/bandit
+ rev: 1.7.4
+ hooks:
+ - id: bandit
+ args: [--severity-level=high]
+ files: ^src/
+
- repo: https://github.com/asottile/yesqa
rev: v1.4.0
hooks:
@@ -25,10 +32,11 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8
- rev: 5.0.4
+ rev: 6.0.0
hooks:
- id: flake8
- additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
+ additional_dependencies:
+ [flake8-2020, flake8-errmsg, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0
@@ -37,16 +45,21 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v4.4.0
hooks:
- id: check-merge-conflict
- id: check-json
- id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v0.6.1
+ rev: v0.6.7
hooks:
- id: sphinx-lint
+ - repo: https://github.com/tox-dev/tox-ini-fmt
+ rev: 0.5.2
+ hooks:
+ - id: tox-ini-fmt
+
ci:
autoupdate_schedule: monthly
diff --git a/CHANGES.rst b/CHANGES.rst
index c3e60acff..904c73629 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,9 +2,192 @@
Changelog (Pillow)
==================
-9.3.0 (unreleased)
+9.4.0 (unreleased)
------------------
+- Improve exception traceback readability #6836
+ [hugovk, radarhere]
+
+- Do not attempt to read IFD1 if absent #6840
+ [radarhere]
+
+- Fixed writing int as ASCII tag #6800
+ [radarhere]
+
+- If available, use wl-paste or xclip for grabclipboard() on Linux #6783
+ [radarhere]
+
+- Added signed option when saving JPEG2000 images #6709
+ [radarhere]
+
+- Patch OpenJPEG to include ARM64 fix #6718
+ [radarhere]
+
+- Added support for I;16 modes in putdata() #6825
+ [radarhere]
+
+- Added conversion from RGBa to RGB #6708
+ [radarhere]
+
+- Added DDS support for uncompressed L and LA images #6820
+ [radarhere, REDxEYE]
+
+- Added LightSource tag values to ExifTags #6749
+ [radarhere]
+
+- Fixed PyAccess after changing ICO size #6821
+ [radarhere]
+
+- Do not use EXIF from info when saving PNG images #6819
+ [radarhere]
+
+- Fixed saving EXIF data to MPO #6817
+ [radarhere]
+
+- Added Exif hide_offsets() #6762
+ [radarhere]
+
+- Only compare to previous frame when checking for duplicate GIF frames while saving #6787
+ [radarhere]
+
+- Always initialize all plugins in registered_extensions() #6811
+ [radarhere]
+
+- Ignore non-opaque WebP background when saving as GIF #6792
+ [radarhere]
+
+- Only set tile in ImageFile __setstate__ #6793
+ [radarhere]
+
+- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767
+ [radarhere]
+
+- Added IFD enum to ExifTags #6748
+ [radarhere]
+
+- Fixed bug combining GIF frame durations #6779
+ [radarhere]
+
+- Support saving JPEG comments #6774
+ [smason, radarhere]
+
+- Added getxmp() to WebPImagePlugin #6758
+ [radarhere]
+
+- Added "exact" option when saving WebP #6747
+ [ashafaei, radarhere]
+
+- Use fractional coordinates when drawing text #6722
+ [radarhere]
+
+- Fixed writing int as BYTE tag #6740
+ [radarhere]
+
+- Added MP Format Version when saving MPO #6735
+ [radarhere]
+
+- Added Interop to ExifTags #6724
+ [radarhere]
+
+- CVE-2007-4559 patch when building on Windows #6704
+ [TrellixVulnTeam, nulano, radarhere]
+
+- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714
+ [wiredfool]
+
+- Use verbose flag for pip install #6713
+ [wiredfool, radarhere]
+
+9.3.0 (2022-10-29)
+------------------
+
+- Limit SAMPLESPERPIXEL to avoid runtime DOS #6700
+ [wiredfool]
+
+- Initialize libtiff buffer when saving #6699
+ [radarhere]
+
+- Inline fname2char to fix memory leak #6329
+ [nulano]
+
+- Fix memory leaks related to text features #6330
+ [nulano]
+
+- Use double quotes for version check on old CPython on Windows #6695
+ [hugovk]
+
+- Remove backup implementation of Round for Windows platforms #6693
+ [cgohlke]
+
+- Fixed set_variation_by_name offset #6445
+ [radarhere]
+
+- Fix malloc in _imagingft.c:font_setvaraxes #6690
+ [cgohlke]
+
+- Release Python GIL when converting images using matrix operations #6418
+ [hmaarrfk]
+
+- Added ExifTags enums #6630
+ [radarhere]
+
+- Do not modify previous frame when calculating delta in PNG #6683
+ [radarhere]
+
+- Added support for reading BMP images with RLE4 compression #6674
+ [npjg, radarhere]
+
+- Decode JPEG compressed BLP1 data in original mode #6678
+ [radarhere]
+
+- Added GPS TIFF tag info #6661
+ [radarhere]
+
+- Added conversion between RGB/RGBA/RGBX and LAB #6647
+ [radarhere]
+
+- Do not attempt normalization if mode is already normal #6644
+ [radarhere]
+
+- Fixed seeking to an L frame in a GIF #6576
+ [radarhere]
+
+- Consider all frames when selecting mode for PNG save_all #6610
+ [radarhere]
+
+- Don't reassign crc on ChunkStream close #6627
+ [wiredfool, radarhere]
+
+- Raise a warning if NumPy failed to raise an error during conversion #6594
+ [radarhere]
+
+- Show all frames in ImageShow #6611
+ [radarhere]
+
+- Allow FLI palette chunk to not be first #6626
+ [radarhere]
+
+- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592
+ [radarhere]
+
+- Round box position to integer when pasting embedded color #6517
+ [radarhere, nulano]
+
+- Removed EXIF prefix when saving WebP #6582
+ [radarhere]
+
+- Pad IM palette to 768 bytes when saving #6579
+ [radarhere]
+
+- Added DDS BC6H reading #6449
+ [ShadelessFox, REDxEYE, radarhere]
+
+- Added support for opening WhiteIsZero 16-bit integer TIFF images #6642
+ [JayWiz, radarhere]
+
+- Raise an error when allocating translucent color to RGB palette #6654
+ [jsbueno, radarhere]
+
- Added reading of TIFF child images #6569
[radarhere]
diff --git a/MANIFEST.in b/MANIFEST.in
index 08f6dfc08..f51551303 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,7 +1,6 @@
include *.c
include *.h
include *.in
-include *.lock
include *.md
include *.py
include *.rst
@@ -10,7 +9,6 @@ include *.txt
include *.yaml
include LICENSE
include Makefile
-include Pipfile
include tox.ini
graft Tests
graft src
diff --git a/Makefile b/Makefile
index 8f2862948..a2545b54e 100644
--- a/Makefile
+++ b/Makefile
@@ -53,12 +53,12 @@ inplace: clean
.PHONY: install
install:
- python3 -m pip install .
+ python3 -m pip -v install .
python3 selftest.py
.PHONY: install-coverage
install-coverage:
- CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" .
+ CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" .
python3 selftest.py
.PHONY: debug
@@ -67,7 +67,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 -m pip install --global-option="build_ext" . > /dev/null
+ CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null
.PHONY: release-test
release-test:
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 1e611a63c..000000000
--- a/Pipfile
+++ /dev/null
@@ -1,22 +0,0 @@
-[[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
deleted file mode 100644
index 600b19050..000000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,324 +0,0 @@
-{
- "_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 e7c0ebc5a..8ee68f9b8 100644
--- a/README.md
+++ b/README.md
@@ -54,9 +54,9 @@ As of 2019, Pillow development is
-
+
diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt
index 104ff677c..da559b3d3 100644
--- a/Tests/fonts/LICENSE.txt
+++ b/Tests/fonts/LICENSE.txt
@@ -8,6 +8,7 @@ TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
ter-x20b.pcf, from http://terminus-font.sourceforge.net/
BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee
+OpenSans.woff2, from https://fonts.googleapis.com/css?family=Open+Sans
All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to.
diff --git a/Tests/fonts/OpenSans.woff2 b/Tests/fonts/OpenSans.woff2
new file mode 100644
index 000000000..15339ea9c
Binary files /dev/null and b/Tests/fonts/OpenSans.woff2 differ
diff --git a/Tests/helper.py b/Tests/helper.py
index 13c6955e4..0d1d03ac8 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -208,12 +208,11 @@ class PillowLeakTestCase:
# ru_maxrss
# This is the maximum resident set size utilized (in bytes).
return mem / 1024 # Kb
- else:
- # linux
- # man 2 getrusage
- # ru_maxrss (since Linux 2.6.32)
- # This is the maximum resident set size used (in kilobytes).
- return mem # Kb
+ # linux
+ # man 2 getrusage
+ # ru_maxrss (since Linux 2.6.32)
+ # This is the maximum resident set size used (in kilobytes).
+ return mem # Kb
def _test_leak(self, core):
start_mem = self._get_mem_usage()
@@ -285,7 +284,7 @@ def magick_command():
if imagemagick and shutil.which(imagemagick[0]):
return imagemagick
- elif graphicsmagick and shutil.which(graphicsmagick[0]):
+ if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick
diff --git a/Tests/images/bc6h.dds b/Tests/images/bc6h.dds
new file mode 100644
index 000000000..77993a0c1
Binary files /dev/null and b/Tests/images/bc6h.dds differ
diff --git a/Tests/images/bc6h.png b/Tests/images/bc6h.png
new file mode 100644
index 000000000..609f11489
Binary files /dev/null and b/Tests/images/bc6h.png differ
diff --git a/Tests/images/bc6h_sf.dds b/Tests/images/bc6h_sf.dds
new file mode 100644
index 000000000..2ab1b195b
Binary files /dev/null and b/Tests/images/bc6h_sf.dds differ
diff --git a/Tests/images/bc6h_sf.png b/Tests/images/bc6h_sf.png
new file mode 100644
index 000000000..6a3b73d5f
Binary files /dev/null and b/Tests/images/bc6h_sf.png differ
diff --git a/Tests/images/blp/blp1_jpeg2.blp b/Tests/images/blp/blp1_jpeg2.blp
new file mode 100644
index 000000000..890180e9b
Binary files /dev/null and b/Tests/images/blp/blp1_jpeg2.blp differ
diff --git a/Tests/images/bw_gradient.imt b/Tests/images/bw_gradient.imt
new file mode 100644
index 000000000..d765cf95f
Binary files /dev/null and b/Tests/images/bw_gradient.imt differ
diff --git a/Tests/images/duplicate_frame.gif b/Tests/images/duplicate_frame.gif
new file mode 100644
index 000000000..ef0c894a5
Binary files /dev/null and b/Tests/images/duplicate_frame.gif differ
diff --git a/Tests/images/flower_thumbnail.png b/Tests/images/flower_thumbnail.png
new file mode 100644
index 000000000..4a362535f
Binary files /dev/null and b/Tests/images/flower_thumbnail.png differ
diff --git a/Tests/images/hopper_lzma.tif b/Tests/images/hopper_lzma.tif
new file mode 100644
index 000000000..d7ca089fc
Binary files /dev/null and b/Tests/images/hopper_lzma.tif differ
diff --git a/Tests/images/hopper_palette_chunk_second.fli b/Tests/images/hopper_palette_chunk_second.fli
new file mode 100644
index 000000000..54447de0a
Binary files /dev/null and b/Tests/images/hopper_palette_chunk_second.fli differ
diff --git a/Tests/images/hopper_webp.png b/Tests/images/hopper_webp.png
new file mode 100644
index 000000000..94b927ac2
Binary files /dev/null and b/Tests/images/hopper_webp.png differ
diff --git a/Tests/images/hopper_webp.tif b/Tests/images/hopper_webp.tif
new file mode 100644
index 000000000..5e398606c
Binary files /dev/null and b/Tests/images/hopper_webp.tif differ
diff --git a/Tests/images/no_palette_after_rgb.gif b/Tests/images/no_palette_after_rgb.gif
new file mode 100644
index 000000000..8704c464c
Binary files /dev/null and b/Tests/images/no_palette_after_rgb.gif differ
diff --git a/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif
new file mode 100644
index 000000000..01dca594f
Binary files /dev/null and b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif differ
diff --git a/Tests/images/palette_not_needed_for_second_frame.gif b/Tests/images/palette_not_needed_for_second_frame.gif
new file mode 100644
index 000000000..0617291d1
Binary files /dev/null and b/Tests/images/palette_not_needed_for_second_frame.gif differ
diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png
index cf002b12c..7e98b8eac 100644
Binary files a/Tests/images/test_anchor_multiline_mm_right.png and b/Tests/images/test_anchor_multiline_mm_right.png differ
diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png
index 7b1e9c4e4..6a1513024 100644
Binary files a/Tests/images/test_combine_multiline_lm_center.png and b/Tests/images/test_combine_multiline_lm_center.png differ
diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png
index a26996c2d..8eb254fdf 100644
Binary files a/Tests/images/test_combine_multiline_lm_left.png and b/Tests/images/test_combine_multiline_lm_left.png differ
diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png
index 7caf5cb74..cb640a740 100644
Binary files a/Tests/images/test_combine_multiline_lm_right.png and b/Tests/images/test_combine_multiline_lm_right.png differ
diff --git a/Tests/images/test_combine_multiline_mm_center.png b/Tests/images/test_combine_multiline_mm_center.png
index a859e9570..d1146b8b8 100644
Binary files a/Tests/images/test_combine_multiline_mm_center.png and b/Tests/images/test_combine_multiline_mm_center.png differ
diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png
index aadb5191f..f539a8e62 100644
Binary files a/Tests/images/test_combine_multiline_mm_left.png and b/Tests/images/test_combine_multiline_mm_left.png differ
diff --git a/Tests/images/test_combine_multiline_mm_right.png b/Tests/images/test_combine_multiline_mm_right.png
index 8238d4ec8..02634163e 100644
Binary files a/Tests/images/test_combine_multiline_mm_right.png and b/Tests/images/test_combine_multiline_mm_right.png differ
diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png
index 7568dd63a..4cce8f6a0 100644
Binary files a/Tests/images/test_combine_multiline_rm_center.png and b/Tests/images/test_combine_multiline_rm_center.png differ
diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png
index b8c3b5b14..93d8162b3 100644
Binary files a/Tests/images/test_combine_multiline_rm_left.png and b/Tests/images/test_combine_multiline_rm_left.png differ
diff --git a/Tests/images/test_combine_multiline_rm_right.png b/Tests/images/test_combine_multiline_rm_right.png
index 14c478a72..6c4634560 100644
Binary files a/Tests/images/test_combine_multiline_rm_right.png and b/Tests/images/test_combine_multiline_rm_right.png differ
diff --git a/Tests/images/test_woff2.png b/Tests/images/test_woff2.png
new file mode 100644
index 000000000..4eb3be4c7
Binary files /dev/null and b/Tests/images/test_woff2.png differ
diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png
new file mode 100644
index 000000000..d2270826a
Binary files /dev/null and b/Tests/images/text_float_coord.png differ
diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png
new file mode 100644
index 000000000..2287071ff
Binary files /dev/null and b/Tests/images/text_float_coord_1_alt.png differ
diff --git a/Tests/images/uncompressed_l.dds b/Tests/images/uncompressed_l.dds
new file mode 100644
index 000000000..b82282587
Binary files /dev/null and b/Tests/images/uncompressed_l.dds differ
diff --git a/Tests/images/uncompressed_l.png b/Tests/images/uncompressed_l.png
new file mode 100644
index 000000000..9d22a26a4
Binary files /dev/null and b/Tests/images/uncompressed_l.png differ
diff --git a/Tests/images/uncompressed_la.dds b/Tests/images/uncompressed_la.dds
new file mode 100644
index 000000000..30bf93576
Binary files /dev/null and b/Tests/images/uncompressed_la.dds differ
diff --git a/Tests/images/uncompressed_la.png b/Tests/images/uncompressed_la.png
new file mode 100644
index 000000000..0d4ea602f
Binary files /dev/null and b/Tests/images/uncompressed_la.png differ
diff --git a/Tests/images/unimplemented_dxgi_format.dds b/Tests/images/unimplemented_dxgi_format.dds
index 5ecb42006..70860f2fc 100644
Binary files a/Tests/images/unimplemented_dxgi_format.dds and b/Tests/images/unimplemented_dxgi_format.dds differ
diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png
index 11ceaf6e6..5168e04b9 100644
Binary files a/Tests/images/variation_adobe_name.png and b/Tests/images/variation_adobe_name.png differ
diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png
index 2adb517a7..fa0e307b4 100644
Binary files a/Tests/images/variation_adobe_older_harfbuzz_name.png and b/Tests/images/variation_adobe_older_harfbuzz_name.png differ
diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh
index 09cc7bc16..7e9098f53 100755
--- a/Tests/oss-fuzz/build.sh
+++ b/Tests/oss-fuzz/build.sh
@@ -19,29 +19,17 @@ python3 setup.py build --build-base=/tmp/build install
# Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
- fuzzer_basename=$(basename -s .py $fuzzer)
- fuzzer_package=${fuzzer_basename}.pkg
- pyinstaller \
+ compile_python_fuzzer $fuzzer \
--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:. \
--add-binary /usr/local/lib/libpng16.so.16:. \
- --add-binary /usr/local/lib/libtiff.so.5:. \
+ --add-binary /usr/local/lib/libtiff.so.6:. \
--add-binary /usr/local/lib/libwebp.so.7:. \
--add-binary /usr/local/lib/libwebpdemux.so.2:. \
--add-binary /usr/local/lib/libwebpmux.so.3:. \
- --add-binary /usr/local/lib/libxcb.so.1:. \
- --distpath $OUT --onefile --name $fuzzer_package $fuzzer
-
- # Create execution wrapper.
- echo "#!/bin/sh
-# LLVMFuzzerTestOneInput for fuzzer detection.
-this_dir=\$(dirname \"\$0\")
-LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \
-ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \
-\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename
- chmod u+x $OUT/$fuzzer_basename
+ --add-binary /usr/local/lib/libxcb.so.1:.
done
find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@
diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py
index b17aad2ea..ed9aff9cc 100644
--- a/Tests/test_bmp_reference.py
+++ b/Tests/test_bmp_reference.py
@@ -35,6 +35,7 @@ def test_questionable():
"pal8os2v2.bmp",
"rgb24prof.bmp",
"pal1p1.bmp",
+ "pal4rletrns.bmp",
"pal8offs.bmp",
"rgb24lprof.bmp",
"rgb32fakealpha.bmp",
diff --git a/Tests/test_features.py b/Tests/test_features.py
index 284f72205..c4e9cd368 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -70,14 +70,14 @@ def test_libimagequant_version():
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
-def test_check_modules():
- for feature in features.modules:
- assert features.check_module(feature) in [True, False]
+@pytest.mark.parametrize("feature", features.modules)
+def test_check_modules(feature):
+ assert features.check_module(feature) in [True, False]
-def test_check_codecs():
- for feature in features.codecs:
- assert features.check_codec(feature) in [True, False]
+@pytest.mark.parametrize("feature", features.codecs)
+def test_check_codecs(feature):
+ assert features.check_codec(feature) in [True, False]
def test_check_warns_on_nonexistent():
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 0ff05f608..51637c786 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -39,13 +39,12 @@ def test_apng_basic():
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-def test_apng_fdat():
- with Image.open("Tests/images/apng/split_fdat.png") as im:
- im.seek(im.n_frames - 1)
- assert im.getpixel((0, 0)) == (0, 255, 0, 255)
- assert im.getpixel((64, 32)) == (0, 255, 0, 255)
-
- with Image.open("Tests/images/apng/split_fdat_zero_chunk.png") as im:
+@pytest.mark.parametrize(
+ "filename",
+ ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
+)
+def test_apng_fdat(filename):
+ with Image.open(filename) as im:
im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@@ -554,18 +553,20 @@ def test_apng_save_disposal(tmp_path):
def test_apng_save_disposal_previous(tmp_path):
test_file = str(tmp_path / "temp.png")
size = (128, 64)
- transparent = Image.new("RGBA", size, (0, 0, 0, 0))
+ blue = Image.new("RGBA", size, (0, 0, 255, 255))
red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255))
# test OP_NONE
- transparent.save(
+ blue.save(
test_file,
save_all=True,
append_images=[red, green],
disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
)
with Image.open(test_file) as im:
+ assert im.getpixel((0, 0)) == (0, 0, 255, 255)
+
im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
@@ -648,6 +649,16 @@ def test_seek_after_close():
im.seek(0)
+@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
+def test_different_modes_in_later_frames(mode, tmp_path):
+ test_file = str(tmp_path / "temp.png")
+
+ im = Image.new("L", (1, 1))
+ im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
+ with Image.open(test_file) as reloaded:
+ assert reloaded.mode == mode
+
+
def test_constants_deprecation():
for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_",
diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py
index c1fae44ca..ba2781820 100644
--- a/Tests/test_file_blp.py
+++ b/Tests/test_file_blp.py
@@ -14,6 +14,9 @@ def test_load_blp1():
with Image.open("Tests/images/blp/blp1_jpeg.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
+ with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
+ im.load()
+
def test_load_blp2_raw():
with Image.open("Tests/images/blp/blp2_raw.blp") as im:
diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py
index f6860a9a4..5f6d52355 100644
--- a/Tests/test_file_bmp.py
+++ b/Tests/test_file_bmp.py
@@ -176,6 +176,11 @@ def test_rle8():
im.load()
+def test_rle4():
+ with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im:
+ assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12)
+
+
@pytest.mark.parametrize(
"file_name,length",
(
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
index 351001199..cac4108a8 100644
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -16,10 +16,14 @@ 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_BC6H = "Tests/images/bc6h.dds"
+TEST_FILE_BC6HS = "Tests/images/bc6h_sf.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
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_L = "Tests/images/uncompressed_l.dds"
+TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@@ -114,6 +118,20 @@ def test_dx10_bc5(image_path, expected_path):
assert_image_equal_tofile(im, expected_path.replace(".dds", ".png"))
+@pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS))
+def test_dx10_bc6h(image_path):
+ """Check DX10 BC6H/BC6HS images can be opened"""
+
+ with Image.open(image_path) as im:
+ im.load()
+
+ assert im.format == "DDS"
+ assert im.mode == "RGB"
+ assert im.size == (128, 128)
+
+ assert_image_equal_tofile(im, image_path.replace(".dds", ".png"))
+
+
def test_dx10_bc7():
"""Check DX10 images can be opened"""
@@ -178,26 +196,24 @@ def test_unimplemented_dxgi_format():
pass
-def test_uncompressed_rgb():
- """Check uncompressed RGB images can be opened"""
+@pytest.mark.parametrize(
+ ("mode", "size", "test_file"),
+ [
+ ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
+ ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
+ ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
+ ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
+ ],
+)
+def test_uncompressed(mode, size, test_file):
+ """Check uncompressed images can be opened"""
- # convert -format dds -define dds:compression=none hopper.jpg hopper.dds
- with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im:
+ with Image.open(test_file) as im:
assert im.format == "DDS"
- assert im.mode == "RGB"
- assert im.size == (128, 128)
+ assert im.mode == mode
+ assert im.size == size
- 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_WITH_ALPHA.replace(".dds", ".png")
- )
+ assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))
def test__accept_true():
@@ -289,6 +305,8 @@ def test_save_unsupported_mode(tmp_path):
@pytest.mark.parametrize(
("mode", "test_file"),
[
+ ("L", "Tests/images/linear_gradient.png"),
+ ("LA", "Tests/images/uncompressed_la.png"),
("RGB", "Tests/images/hopper.png"),
("RGBA", "Tests/images/pil123rgba.png"),
],
diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py
index 766c50649..015dda992 100644
--- a/Tests/test_file_eps.py
+++ b/Tests/test_file_eps.py
@@ -124,14 +124,6 @@ def test_file_object(tmp_path):
image1.save(fh, "EPS")
-@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_iobase_object(tmp_path):
- # issue 479
- with Image.open(FILE1) as image1:
- with open(str(tmp_path / "temp_iobase.eps"), "wb") as fh:
- image1.save(fh, "EPS")
-
-
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_bytesio_object():
with open(FILE1, "rb") as f:
@@ -203,25 +195,23 @@ def test_render_scale2():
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_resize():
- files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"]
- for fn in files:
- with Image.open(fn) as im:
- new_size = (100, 100)
- im = im.resize(new_size)
- assert im.size == new_size
+@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
+def test_resize(filename):
+ with Image.open(filename) as im:
+ new_size = (100, 100)
+ im = im.resize(new_size)
+ assert im.size == new_size
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
-def test_thumbnail():
+@pytest.mark.parametrize("filename", (FILE1, FILE2))
+def test_thumbnail(filename):
# Issue #619
# Arrange
- files = [FILE1, FILE2]
- for fn in files:
- with Image.open(FILE1) as im:
- new_size = (100, 100)
- im.thumbnail(new_size)
- assert max(im.size) == max(new_size)
+ with Image.open(filename) as im:
+ new_size = (100, 100)
+ im.thumbnail(new_size)
+ assert max(im.size) == max(new_size)
def test_read_binary_preview():
@@ -266,20 +256,19 @@ def test_readline(tmp_path):
_test_readline_file_psfile(s, ending)
-def test_open_eps():
- # https://github.com/python-pillow/Pillow/issues/1104
- # Arrange
- FILES = [
+@pytest.mark.parametrize(
+ "filename",
+ (
"Tests/images/illu10_no_preview.eps",
"Tests/images/illu10_preview.eps",
"Tests/images/illuCS6_no_preview.eps",
"Tests/images/illuCS6_preview.eps",
- ]
-
- # Act / Assert
- for filename in FILES:
- with Image.open(filename) as img:
- assert img.mode == "RGB"
+ ),
+)
+def test_open_eps(filename):
+ # https://github.com/python-pillow/Pillow/issues/1104
+ with Image.open(filename) as img:
+ assert img.mode == "RGB"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py
index a7d43d2e9..b8b999d70 100644
--- a/Tests/test_file_fli.py
+++ b/Tests/test_file_fli.py
@@ -4,7 +4,7 @@ import pytest
from PIL import FliImagePlugin, Image
-from .helper import assert_image_equal_tofile, is_pypy
+from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# created as an export of a palette image from Gimp2.6
# save as...-> hopper.fli, default options.
@@ -79,6 +79,12 @@ def test_invalid_file():
FliImagePlugin.FliImageFile(invalid_file)
+def test_palette_chunk_second():
+ with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
+ with Image.open(static_test_file) as expected:
+ assert_image_equal(im.convert("RGB"), expected.convert("RGB"))
+
+
def test_n_frames():
with Image.open(static_test_file) as im:
assert im.n_frames == 1
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 4e967faec..d48fc1442 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -83,18 +83,40 @@ def test_l_mode_transparency():
assert im.load()[0, 0] == 128
+def test_l_mode_after_rgb():
+ with Image.open("Tests/images/no_palette_after_rgb.gif") as im:
+ im.seek(1)
+ assert im.mode == "RGB"
+
+ im.seek(2)
+ assert im.mode == "RGB"
+
+
+def test_palette_not_needed_for_second_frame():
+ with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
+ im.seek(1)
+ assert_image_similar(im, hopper("L").convert("RGB"), 8)
+
+
def test_strategy():
+ with Image.open("Tests/images/iss634.gif") as im:
+ expected_rgb_always = im.convert("RGB")
+
with Image.open("Tests/images/chi.gif") as im:
- expected_zero = im.convert("RGB")
+ expected_rgb_always_rgba = im.convert("RGBA")
im.seek(1)
- expected_one = im.convert("RGB")
+ expected_different = im.convert("RGB")
try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
- with Image.open("Tests/images/chi.gif") as im:
+ with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB"
- assert_image_equal(im, expected_zero)
+ assert_image_equal(im, expected_rgb_always)
+
+ with Image.open("Tests/images/chi.gif") as im:
+ assert im.mode == "RGBA"
+ assert_image_equal(im, expected_rgb_always_rgba)
GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
@@ -105,7 +127,7 @@ def test_strategy():
im.seek(1)
assert im.mode == "P"
- assert_image_equal(im.convert("RGB"), expected_one)
+ assert_image_equal(im.convert("RGB"), expected_different)
# Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im:
@@ -655,6 +677,24 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0)
+def test_dispose2_background_frame(tmp_path):
+ out = str(tmp_path / "temp.gif")
+
+ im_list = [Image.new("RGBA", (1, 20))]
+
+ different_frame = Image.new("RGBA", (1, 20))
+ different_frame.putpixel((0, 10), (255, 0, 0, 255))
+ im_list.append(different_frame)
+
+ # Frame that matches the background
+ im_list.append(Image.new("RGBA", (1, 20)))
+
+ im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
+
+ with Image.open(out) as im:
+ assert im.n_frames == 3
+
+
def test_transparency_in_second_frame(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
@@ -769,6 +809,22 @@ def test_roundtrip_info_duration(tmp_path):
] == duration_list
+def test_roundtrip_info_duration_combined(tmp_path):
+ out = str(tmp_path / "temp.gif")
+ with Image.open("Tests/images/duplicate_frame.gif") as im:
+ assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
+ 1000,
+ 1000,
+ 1000,
+ ]
+ im.save(out, save_all=True)
+
+ with Image.open(out) as reloaded:
+ assert [
+ frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
+ ] == [1000, 2000]
+
+
def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000]
@@ -793,24 +849,24 @@ def test_identical_frames(tmp_path):
assert reread.info["duration"] == 4500
-def test_identical_frames_to_single_frame(tmp_path):
- for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500):
- out = str(tmp_path / "temp.gif")
- im_list = [
- Image.new("L", (100, 100), "#000"),
- Image.new("L", (100, 100), "#000"),
- Image.new("L", (100, 100), "#000"),
- ]
+@pytest.mark.parametrize(
+ "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)
+)
+def test_identical_frames_to_single_frame(duration, tmp_path):
+ out = str(tmp_path / "temp.gif")
+ im_list = [
+ Image.new("L", (100, 100), "#000"),
+ Image.new("L", (100, 100), "#000"),
+ Image.new("L", (100, 100), "#000"),
+ ]
- im_list[0].save(
- out, save_all=True, append_images=im_list[1:], duration=duration
- )
- with Image.open(out) as reread:
- # Assert that all frames were combined
- assert reread.n_frames == 1
+ im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)
+ with Image.open(out) as reread:
+ # Assert that all frames were combined
+ assert reread.n_frames == 1
- # Assert that the new duration is the total of the identical frames
- assert reread.info["duration"] == 8500
+ # Assert that the new duration is the total of the identical frames
+ assert reread.info["duration"] == 8500
def test_number_of_loops(tmp_path):
@@ -837,14 +893,23 @@ def test_background(tmp_path):
im.info["background"] = 1
im.save(out)
with Image.open(out) as reread:
-
assert reread.info["background"] == im.info["background"]
+
+def test_webp_background(tmp_path):
+ out = str(tmp_path / "temp.gif")
+
+ # Test opaque WebP background
if features.check("webp") and features.check("webp_anim"):
with Image.open("Tests/images/hopper.webp") as im:
- assert isinstance(im.info["background"], tuple)
+ assert im.info["background"] == (255, 255, 255, 255)
im.save(out)
+ # Test non-opaque WebP background
+ im = Image.new("L", (100, 100), "#000")
+ im.info["background"] = (0, 0, 0, 0)
+ im.save(out)
+
def test_comment(tmp_path):
with Image.open(TEST_GIF) as im:
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 3fcd5c61f..afb17b1af 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -71,6 +71,19 @@ def test_save_to_bytes():
)
+def test_getpixel(tmp_path):
+ temp_file = str(tmp_path / "temp.ico")
+
+ im = hopper()
+ im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
+
+ with Image.open(temp_file) as reloaded:
+ reloaded.load()
+ reloaded.size = (32, 32)
+
+ assert reloaded.getpixel((0, 0)) == (18, 20, 62)
+
+
def test_no_duplicates(tmp_path):
temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico")
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
index e458a197c..5cf93713b 100644
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -86,6 +86,18 @@ def test_roundtrip(mode, tmp_path):
assert_image_equal_tofile(im, out)
+def test_small_palette(tmp_path):
+ im = Image.new("P", (1, 1))
+ colors = [0, 1, 2]
+ im.putpalette(colors)
+
+ out = str(tmp_path / "temp.im")
+ im.save(out)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.getpalette() == colors + [0] * 765
+
+
def test_save_unsupported_mode(tmp_path):
out = str(tmp_path / "temp.im")
im = hopper("HSV")
diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py
new file mode 100644
index 000000000..f56acc429
--- /dev/null
+++ b/Tests/test_file_imt.py
@@ -0,0 +1,19 @@
+import io
+
+import pytest
+
+from PIL import Image, ImtImagePlugin
+
+from .helper import assert_image_equal_tofile
+
+
+def test_sanity():
+ with Image.open("Tests/images/bw_gradient.imt") as im:
+ assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
+
+
+@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n"))
+def test_invalid_file(data):
+ with io.BytesIO(data) as fp:
+ with pytest.raises(SyntaxError):
+ ImtImagePlugin.ImtImageFile(fp)
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 12edd7582..eabc6bf75 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -30,7 +30,7 @@ from .helper import (
)
try:
- import defusedxml.ElementTree as ElementTree
+ from defusedxml import ElementTree
except ImportError:
ElementTree = None
@@ -86,6 +86,33 @@ class TestFileJpeg:
assert len(im.applist) == 2
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+ assert im.app["COM"] == im.info["comment"]
+
+ def test_comment_write(self):
+ with Image.open(TEST_FILE) as im:
+ assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00"
+
+ # Test that existing comment is saved by default
+ out = BytesIO()
+ im.save(out, format="JPEG")
+ with Image.open(out) as reloaded:
+ assert im.info["comment"] == reloaded.info["comment"]
+
+ # Ensure that a blank comment causes any existing comment to be removed
+ for comment in ("", b"", None):
+ out = BytesIO()
+ im.save(out, format="JPEG", comment=comment)
+ with Image.open(out) as reloaded:
+ assert "comment" not in reloaded.info
+
+ # Test that a comment argument overrides the default comment
+ for comment in ("Test comment text", b"Text comment text"):
+ out = BytesIO()
+ im.save(out, format="JPEG", comment=comment)
+ with Image.open(out) as reloaded:
+ if not isinstance(comment, bytes):
+ comment = comment.encode()
+ assert reloaded.info["comment"] == comment
def test_cmyk(self):
# Test CMYK handling. Thanks to Tim and Charlie for test data,
@@ -150,27 +177,30 @@ class TestFileJpeg:
assert not im1.info.get("icc_profile")
assert im2.info.get("icc_profile")
- def test_icc_big(self):
+ @pytest.mark.parametrize(
+ "n",
+ (
+ 0,
+ 1,
+ 3,
+ 4,
+ 5,
+ 65533 - 14, # full JPEG marker block
+ 65533 - 14 + 1, # full block plus one byte
+ ImageFile.MAXBLOCK, # full buffer block
+ ImageFile.MAXBLOCK + 1, # full buffer block plus one byte
+ ImageFile.MAXBLOCK * 4 + 3, # large block
+ ),
+ )
+ def test_icc_big(self, n):
# Make sure that the "extra" support handles large blocks
- def test(n):
- # The ICC APP marker can store 65519 bytes per marker, so
- # using a 4-byte test code should allow us to detect out of
- # order issues.
- icc_profile = (b"Test" * int(n / 4 + 1))[:n]
- assert len(icc_profile) == n # sanity
- im1 = self.roundtrip(hopper(), icc_profile=icc_profile)
- assert im1.info.get("icc_profile") == (icc_profile or None)
-
- test(0)
- test(1)
- test(3)
- test(4)
- test(5)
- test(65533 - 14) # full JPEG marker block
- test(65533 - 14 + 1) # full block plus one byte
- test(ImageFile.MAXBLOCK) # full buffer block
- test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte
- test(ImageFile.MAXBLOCK * 4 + 3) # large block
+ # The ICC APP marker can store 65519 bytes per marker, so
+ # using a 4-byte test code should allow us to detect out of
+ # order issues.
+ icc_profile = (b"Test" * int(n / 4 + 1))[:n]
+ assert len(icc_profile) == n # sanity
+ im1 = self.roundtrip(hopper(), icc_profile=icc_profile)
+ assert im1.info.get("icc_profile") == (icc_profile or None)
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@@ -412,6 +442,13 @@ class TestFileJpeg:
info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh"
+ def test_get_child_images(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ ims = im.get_child_images()
+
+ assert len(ims) == 1
+ assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png")
+
def test_mp(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im._getmp() is None
@@ -649,19 +686,19 @@ class TestFileJpeg:
# Assert
assert im.format == "JPEG"
- def test_save_correct_modes(self):
+ @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
+ def test_save_correct_modes(self, mode):
out = BytesIO()
- for mode in ["1", "L", "RGB", "RGBX", "CMYK", "YCbCr"]:
- img = Image.new(mode, (20, 20))
- img.save(out, "JPEG")
+ img = Image.new(mode, (20, 20))
+ img.save(out, "JPEG")
- def test_save_wrong_modes(self):
+ @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P"))
+ def test_save_wrong_modes(self, mode):
# ref https://github.com/python-pillow/Pillow/issues/2005
out = BytesIO()
- for mode in ["LA", "La", "RGBA", "RGBa", "P"]:
- img = Image.new(mode, (20, 20))
- with pytest.raises(OSError):
- img.save(out, "JPEG")
+ img = Image.new(mode, (20, 20))
+ with pytest.raises(OSError):
+ img.save(out, "JPEG")
def test_save_tiff_with_dpi(self, tmp_path):
# Arrange
diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py
index 7942d6b9a..0229b2243 100644
--- a/Tests/test_file_jpeg2k.py
+++ b/Tests/test_file_jpeg2k.py
@@ -126,14 +126,14 @@ 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)
+@pytest.mark.parametrize("num_resolutions", range(2, 6))
+def test_default_num_resolutions(num_resolutions):
+ 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():
@@ -252,6 +252,20 @@ def test_mct():
assert_image_similar(im, jp2, 1.0e-3)
+def test_sgnd(tmp_path):
+ outfile = str(tmp_path / "temp.jp2")
+
+ im = Image.new("L", (1, 1))
+ im.save(outfile)
+ with Image.open(outfile) as reloaded:
+ assert reloaded.getpixel((0, 0)) == 0
+
+ im = Image.new("L", (1, 1))
+ im.save(outfile, signed=True)
+ with Image.open(outfile) as reloaded_signed:
+ assert reloaded_signed.getpixel((0, 0)) == 128
+
+
def test_rgba():
# Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
@@ -266,14 +280,11 @@ def test_rgba():
assert jp2.mode == "RGBA"
-def test_16bit_monochrome_has_correct_mode():
- with Image.open("Tests/images/16bit.cropped.j2k") as j2k:
- j2k.load()
- assert j2k.mode == "I;16"
-
- with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
- jp2.load()
- assert jp2.mode == "I;16"
+@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
+def test_16bit_monochrome_has_correct_mode(ext):
+ with Image.open("Tests/images/16bit.cropped" + ext) as im:
+ im.load()
+ assert im.mode == "I;16"
def test_16bit_monochrome_jp2_like_tiff():
diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py
index 86a0fda04..1109cd15e 100644
--- a/Tests/test_file_libtiff.py
+++ b/Tests/test_file_libtiff.py
@@ -3,6 +3,7 @@ import io
import itertools
import os
import re
+import sys
from collections import namedtuple
import pytest
@@ -509,20 +510,13 @@ class TestFileLibTiff(LibTiffTestCase):
# colormap/palette tag
assert len(reloaded.tag_v2[320]) == 768
- def xtest_bw_compression_w_rgb(self, tmp_path):
- """This test passes, but when running all tests causes a failure due
- to output on stderr from the error thrown by libtiff. We need to
- capture that but not now"""
-
+ @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
+ def test_bw_compression_w_rgb(self, compression, tmp_path):
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
with pytest.raises(OSError):
- im.save(out, compression="tiff_ccitt")
- with pytest.raises(OSError):
- im.save(out, compression="group3")
- with pytest.raises(OSError):
- im.save(out, compression="group4")
+ im.save(out, compression=compression)
def test_fp_leak(self):
im = Image.open("Tests/images/hopper_g4_500.tif")
@@ -832,6 +826,44 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.mode == "F"
assert reloaded.getexif()[SAMPLEFORMAT] == 3
+ def test_lzma(self, capfd):
+ try:
+ with Image.open("Tests/images/hopper_lzma.tif") as im:
+ assert im.mode == "RGB"
+ assert im.size == (128, 128)
+ assert im.format == "TIFF"
+ im2 = hopper()
+ assert_image_similar(im, im2, 5)
+ except OSError:
+ captured = capfd.readouterr()
+ if "LZMA compression support is not configured" in captured.err:
+ pytest.skip("LZMA compression support is not configured")
+ sys.stdout.write(captured.out)
+ sys.stderr.write(captured.err)
+ raise
+
+ def test_webp(self, capfd):
+ try:
+ with Image.open("Tests/images/hopper_webp.tif") as im:
+ assert im.mode == "RGB"
+ assert im.size == (128, 128)
+ assert im.format == "TIFF"
+ assert_image_similar_tofile(im, "Tests/images/hopper_webp.png", 1)
+ except OSError:
+ captured = capfd.readouterr()
+ if "WEBP compression support is not configured" in captured.err:
+ pytest.skip("WEBP compression support is not configured")
+ if (
+ "Compression scheme 50001 strip decoding is not implemented"
+ in captured.err
+ ):
+ pytest.skip(
+ "Compression scheme 50001 strip decoding is not implemented"
+ )
+ sys.stdout.write(captured.out)
+ sys.stderr.write(captured.err)
+ raise
+
def test_lzw(self):
with Image.open("Tests/images/hopper_lzw.tif") as im:
assert im.mode == "RGB"
@@ -941,7 +973,7 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, exif=tags, compression=compression)
with Image.open(out) as reloaded:
- for tag in tags.keys():
+ for tag in tags:
assert tag not in reloaded.getexif()
def test_old_style_jpeg(self):
diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py
index d94bdaa96..3e5476222 100644
--- a/Tests/test_file_mpo.py
+++ b/Tests/test_file_mpo.py
@@ -80,7 +80,10 @@ def test_app(test_file):
@pytest.mark.parametrize("test_file", test_files)
def test_exif(test_file):
- with Image.open(test_file) as im:
+ with Image.open(test_file) as im_original:
+ im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
+
+ for im in (im_original, im_reloaded):
info = im._getexif()
assert info[272] == "Nintendo 3DS"
assert info[296] == 2
@@ -268,6 +271,7 @@ def test_save_all():
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
+ assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)
assert_image_similar(im2, im_reloaded, 1)
diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py
index e1c1c361b..be7c8d0c8 100644
--- a/Tests/test_file_palm.py
+++ b/Tests/test_file_palm.py
@@ -63,19 +63,7 @@ def test_p_mode(tmp_path):
roundtrip(tmp_path, mode)
-def test_l_oserror(tmp_path):
- # Arrange
- mode = "L"
-
- # Act / Assert
- with pytest.raises(OSError):
- helper_save_as_palm(tmp_path, mode)
-
-
-def test_rgb_oserror(tmp_path):
- # Arrange
- mode = "RGB"
-
- # Act / Assert
+@pytest.mark.parametrize("mode", ("L", "RGB"))
+def test_oserror(tmp_path, mode):
with pytest.raises(OSError):
helper_save_as_palm(tmp_path, mode)
diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py
index ba6663cd3..485adf785 100644
--- a/Tests/test_file_pcx.py
+++ b/Tests/test_file_pcx.py
@@ -39,14 +39,14 @@ def test_invalid_file():
PcxImagePlugin.PcxImageFile(invalid_file)
-def test_odd(tmp_path):
+@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
+def test_odd(tmp_path, mode):
# See issue #523, odd sized images should have a stride that's even.
# Not that ImageMagick or GIMP write PCX that way.
# We were not handling properly.
- for mode in ("1", "L", "P", "RGB"):
- # larger, odd sized images are better here to ensure that
- # we handle interrupted scan lines properly.
- _roundtrip(tmp_path, hopper(mode).resize((511, 511)))
+ # larger, odd sized images are better here to ensure that
+ # we handle interrupted scan lines properly.
+ _roundtrip(tmp_path, hopper(mode).resize((511, 511)))
def test_odd_read():
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
index b5273353c..9667b6a4a 100644
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -37,7 +37,11 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs):
return outfile
-@pytest.mark.valgrind_known_error(reason="Temporary skip")
+@pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK"))
+def test_save(tmp_path, mode):
+ helper_save_as_pdf(tmp_path, mode)
+
+
def test_monochrome(tmp_path):
# Arrange
mode = "1"
@@ -47,38 +51,6 @@ def test_monochrome(tmp_path):
assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000)
-def test_greyscale(tmp_path):
- # Arrange
- mode = "L"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
-
-
-def test_rgb(tmp_path):
- # Arrange
- mode = "RGB"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
-
-
-def test_p_mode(tmp_path):
- # Arrange
- mode = "P"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
-
-
-def test_cmyk_mode(tmp_path):
- # Arrange
- mode = "CMYK"
-
- # Act / Assert
- helper_save_as_pdf(tmp_path, mode)
-
-
def test_unsupported_mode(tmp_path):
im = hopper("LA")
outfile = str(tmp_path / "temp_LA.pdf")
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 1af0223eb..9481cd5dd 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -20,7 +20,7 @@ from .helper import (
)
try:
- import defusedxml.ElementTree as ElementTree
+ from defusedxml import ElementTree
except ImportError:
ElementTree = None
@@ -706,10 +706,18 @@ class TestFilePng:
assert exif[274] == 3
def test_exif_save(self, tmp_path):
+ # Test exif is not saved from info
+ test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/exif.png") as im:
- test_file = str(tmp_path / "temp.png")
im.save(test_file)
+ with Image.open(test_file) as reloaded:
+ assert reloaded._getexif() is None
+
+ # Test passing in exif
+ with Image.open("Tests/images/exif.png") as im:
+ im.save(test_file, exif=im.getexif())
+
with Image.open(test_file) as reloaded:
exif = reloaded._getexif()
assert exif[274] == 1
@@ -720,7 +728,7 @@ class TestFilePng:
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")
- im.save(test_file)
+ im.save(test_file, exif=im.getexif())
with Image.open(test_file) as reloaded:
exif = reloaded._getexif()
diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py
index 5c6376caf..fbcbea6c6 100644
--- a/Tests/test_file_ppm.py
+++ b/Tests/test_file_ppm.py
@@ -240,8 +240,8 @@ def test_header_token_too_long(tmp_path):
def test_truncated_file(tmp_path):
# Test EOF in header
path = str(tmp_path / "temp.pgm")
- with open(path, "w") as f:
- f.write("P6")
+ with open(path, "wb") as f:
+ f.write(b"P6")
with pytest.raises(ValueError) as e:
with Image.open(path):
@@ -256,11 +256,11 @@ def test_truncated_file(tmp_path):
im.load()
-@pytest.mark.parametrize("maxval", (0, 65536))
+@pytest.mark.parametrize("maxval", (b"0", b"65536"))
def test_invalid_maxval(maxval, tmp_path):
path = str(tmp_path / "temp.ppm")
- with open(path, "w") as f:
- f.write("P6\n3 1 " + str(maxval))
+ with open(path, "wb") as f:
+ f.write(b"P6\n3 1 " + maxval)
with pytest.raises(ValueError) as e:
with Image.open(path):
@@ -283,13 +283,13 @@ def test_neg_ppm():
def test_mimetypes(tmp_path):
path = str(tmp_path / "temp.pgm")
- with open(path, "w") as f:
- f.write("P4\n128 128\n255")
+ with open(path, "wb") as f:
+ f.write(b"P4\n128 128\n255")
with Image.open(path) as im:
assert im.get_format_mimetype() == "image/x-portable-bitmap"
- with open(path, "w") as f:
- f.write("PyCMYK\n128 128\n255")
+ with open(path, "wb") as f:
+ f.write(b"PyCMYK\n128 128\n255")
with Image.open(path) as im:
assert im.get_format_mimetype() == "image/x-portable-anymap"
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index ac0bd7f60..4f3c8e390 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -4,7 +4,7 @@ from io import BytesIO
import pytest
-from PIL import Image, ImageFile, TiffImagePlugin
+from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
from .helper import (
@@ -18,7 +18,7 @@ from .helper import (
)
try:
- import defusedxml.ElementTree as ElementTree
+ from defusedxml import ElementTree
except ImportError:
ElementTree = None
@@ -311,14 +311,17 @@ class TestFileTiff:
with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"):
pass
- def test_n_frames(self):
- for path, n_frames in [
- ["Tests/images/multipage-lastframe.tif", 1],
- ["Tests/images/multipage.tiff", 3],
- ]:
- with Image.open(path) as im:
- assert im.n_frames == n_frames
- assert im.is_animated == (n_frames != 1)
+ @pytest.mark.parametrize(
+ "path, n_frames",
+ (
+ ("Tests/images/multipage-lastframe.tif", 1),
+ ("Tests/images/multipage.tiff", 3),
+ ),
+ )
+ def test_n_frames(self, path, n_frames):
+ with Image.open(path) as im:
+ assert im.n_frames == n_frames
+ assert im.is_animated == (n_frames != 1)
def test_eoferror(self):
with Image.open("Tests/images/multipage-lastframe.tif") as im:
@@ -434,12 +437,12 @@ class TestFileTiff:
len_after = len(dict(im.ifd))
assert len_before == len_after + 1
- def test_load_byte(self):
- for legacy_api in [False, True]:
- ifd = TiffImagePlugin.ImageFileDirectory_v2()
- data = b"abc"
- ret = ifd.load_byte(data, legacy_api)
- assert ret == b"abc"
+ @pytest.mark.parametrize("legacy_api", (False, True))
+ def test_load_byte(self, legacy_api):
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ data = b"abc"
+ ret = ifd.load_byte(data, legacy_api)
+ assert ret == b"abc"
def test_load_string(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
@@ -685,18 +688,15 @@ class TestFileTiff:
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)
- def test_palette(self, tmp_path):
- def roundtrip(mode):
- outfile = str(tmp_path / "temp.tif")
+ @pytest.mark.parametrize("mode", ("P", "PA"))
+ def test_palette(self, mode, tmp_path):
+ outfile = str(tmp_path / "temp.tif")
- im = hopper(mode)
- im.save(outfile)
+ im = hopper(mode)
+ im.save(outfile)
- with Image.open(outfile) as reloaded:
- assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
-
- for mode in ["P", "PA"]:
- roundtrip(mode)
+ with Image.open(outfile) as reloaded:
+ assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
def test_tiff_save_all(self):
mp = BytesIO()
@@ -858,6 +858,19 @@ class TestFileTiff:
im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False
+ @pytest.mark.parametrize(
+ "test_file",
+ [
+ "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif",
+ ],
+ )
+ @pytest.mark.timeout(2)
+ def test_oom(self, test_file):
+ with pytest.raises(UnidentifiedImageError):
+ with pytest.warns(UserWarning):
+ with Image.open(test_file):
+ pass
+
@pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestFileTiffW32:
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index d38c1c523..48797ea08 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -185,20 +185,37 @@ def test_iptc(tmp_path):
im.save(out)
-def test_writing_bytes_to_ascii(tmp_path):
- im = hopper()
+@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
+def test_writing_other_types_to_ascii(value, expected, tmp_path):
info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271]
assert tag.type == TiffTags.ASCII
- info[271] = b"test"
+ info[271] = value
+
+ im = hopper()
+ out = str(tmp_path / "temp.tiff")
+ im.save(out, tiffinfo=info)
+
+ with Image.open(out) as reloaded:
+ assert reloaded.tag_v2[271] == expected
+
+
+def test_writing_int_to_bytes(tmp_path):
+ im = hopper()
+ info = TiffImagePlugin.ImageFileDirectory_v2()
+
+ tag = TiffTags.TAGS_V2[700]
+ assert tag.type == TiffTags.BYTE
+
+ info[700] = 1
out = str(tmp_path / "temp.tiff")
im.save(out, tiffinfo=info)
with Image.open(out) as reloaded:
- assert reloaded.tag_v2[271] == "test"
+ assert reloaded.tag_v2[700] == b"\x01"
def test_undefined_zero(tmp_path):
diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py
index dc82fb742..5970fd2a3 100644
--- a/Tests/test_file_webp_alpha.py
+++ b/Tests/test_file_webp_alpha.py
@@ -97,6 +97,35 @@ def test_write_rgba(tmp_path):
assert_image_similar(image, pil_image, 1.0)
+def test_keep_rgb_values_when_transparent(tmp_path):
+ """
+ Saving transparent pixels should retain their original RGB values
+ when using the "exact" parameter.
+ """
+
+ image = hopper("RGB")
+
+ # create a copy of the image
+ # with the left half transparent
+ half_transparent_image = image.copy()
+ new_alpha = Image.new("L", (128, 128), 255)
+ new_alpha.paste(0, (0, 0, 64, 128))
+ half_transparent_image.putalpha(new_alpha)
+
+ # save with transparent area preserved
+ temp_file = str(tmp_path / "temp.webp")
+ half_transparent_image.save(temp_file, exact=True, lossless=True)
+
+ with Image.open(temp_file) as reloaded:
+ assert reloaded.mode == "RGBA"
+ assert reloaded.format == "WEBP"
+
+ # even though it is lossless, if we don't use exact=True
+ # in libwebp >= 0.5, the transparent area will be filled with black
+ # (or something more conducive to compression)
+ assert_image_equal(reloaded.convert("RGB"), image)
+
+
def test_write_unsupported_mode_PA(tmp_path):
"""
Saving a palette-based file with transparency to WebP format
diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py
index e6d6fc63f..4f513d82b 100644
--- a/Tests/test_file_webp_metadata.py
+++ b/Tests/test_file_webp_metadata.py
@@ -11,6 +11,11 @@ pytestmark = [
skip_unless_feature("webp_mux"),
]
+try:
+ from defusedxml import ElementTree
+except ImportError:
+ ElementTree = None
+
def test_read_exif_metadata():
@@ -55,9 +60,7 @@ def test_write_exif_metadata():
test_buffer.seek(0)
with Image.open(test_buffer) as webp_image:
webp_exif = webp_image.info.get("exif", None)
- assert webp_exif
- if webp_exif:
- assert webp_exif == expected_exif, "WebP EXIF didn't match"
+ assert webp_exif == expected_exif[6:], "WebP EXIF didn't match"
def test_read_icc_profile():
@@ -112,6 +115,22 @@ def test_read_no_exif():
assert not webp_image._getexif()
+def test_getxmp():
+ with Image.open("Tests/images/flower.webp") as im:
+ assert "xmp" not in im.info
+ assert im.getxmp() == {}
+
+ with Image.open("Tests/images/flower2.webp") as im:
+ if ElementTree is None:
+ with pytest.warns(UserWarning):
+ assert im.getxmp() == {}
+ else:
+ assert (
+ im.getxmp()["xmpmeta"]["xmptk"]
+ == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 "
+ )
+
+
@skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path):
iccp_data = b""
diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py
index 4477ee29d..664663fd6 100644
--- a/Tests/test_font_pcf_charsets.py
+++ b/Tests/test_font_pcf_charsets.py
@@ -1,5 +1,7 @@
import os
+import pytest
+
from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
from .helper import (
@@ -59,23 +61,13 @@ def save_font(request, tmp_path, encoding):
return tempname
-def _test_sanity(request, tmp_path, encoding):
+@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
+def test_sanity(request, tmp_path, encoding):
save_font(request, tmp_path, encoding)
-def test_sanity_iso8859_1(request, tmp_path):
- _test_sanity(request, tmp_path, "iso8859-1")
-
-
-def test_sanity_iso8859_2(request, tmp_path):
- _test_sanity(request, tmp_path, "iso8859-2")
-
-
-def test_sanity_cp1250(request, tmp_path):
- _test_sanity(request, tmp_path, "cp1250")
-
-
-def _test_draw(request, tmp_path, encoding):
+@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
+def test_draw(request, tmp_path, encoding):
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
im = Image.new("L", (150, 30), "white")
@@ -85,19 +77,8 @@ def _test_draw(request, tmp_path, encoding):
assert_image_similar_tofile(im, charsets[encoding]["image1"], 0)
-def test_draw_iso8859_1(request, tmp_path):
- _test_draw(request, tmp_path, "iso8859-1")
-
-
-def test_draw_iso8859_2(request, tmp_path):
- _test_draw(request, tmp_path, "iso8859-2")
-
-
-def test_draw_cp1250(request, tmp_path):
- _test_draw(request, tmp_path, "cp1250")
-
-
-def _test_textsize(request, tmp_path, encoding):
+@pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250"))
+def test_textsize(request, tmp_path, encoding):
tempname = save_font(request, tmp_path, encoding)
font = ImageFont.load(tempname)
for i in range(255):
@@ -112,15 +93,3 @@ def _test_textsize(request, tmp_path, encoding):
msg = message[: i + 1]
assert font.getlength(msg) == len(msg) * 10
assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20)
-
-
-def test_textsize_iso8859_1(request, tmp_path):
- _test_textsize(request, tmp_path, "iso8859-1")
-
-
-def test_textsize_iso8859_2(request, tmp_path):
- _test_textsize(request, tmp_path, "iso8859-2")
-
-
-def test_textsize_cp1250(request, tmp_path):
- _test_textsize(request, tmp_path, "cp1250")
diff --git a/Tests/test_image.py b/Tests/test_image.py
index ab945e946..a37c90296 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -7,7 +7,14 @@ import warnings
import pytest
-from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
+from PIL import (
+ ExifTags,
+ Image,
+ ImageDraw,
+ ImagePalette,
+ UnidentifiedImageError,
+ features,
+)
from .helper import (
assert_image_equal,
@@ -129,8 +136,6 @@ class TestImage:
im.size = (3, 4)
def test_invalid_image(self):
- import io
-
im = io.BytesIO(b"")
with pytest.raises(UnidentifiedImageError):
with Image.open(im):
@@ -396,8 +401,6 @@ class TestImage:
def test_registered_extensions_uninitialized(self):
# Arrange
Image._initialized = 0
- extension = Image.EXTENSION
- Image.EXTENSION = {}
# Act
Image.registered_extensions()
@@ -405,10 +408,6 @@ class TestImage:
# Assert
assert Image._initialized == 2
- # Restore the original state and assert
- Image.EXTENSION = extension
- assert Image.EXTENSION
-
def test_registered_extensions(self):
# Arrange
# Open an image to trigger plugin registration
@@ -699,15 +698,15 @@ class TestImage:
def test_empty_exif(self):
with Image.open("Tests/images/exif.png") as im:
exif = im.getexif()
- assert dict(exif) != {}
+ assert dict(exif)
# Test that exif data is cleared after another load
exif.load(None)
- assert dict(exif) == {}
+ assert not dict(exif)
# Test loading just the EXIF header
exif.load(b"Exif\x00\x00")
- assert dict(exif) == {}
+ assert not dict(exif)
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@@ -810,6 +809,18 @@ class TestImage:
reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005)
+ def test_exif_ifd1(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ exif = im.getexif()
+ assert exif.get_ifd(ExifTags.IFD.IFD1) == {
+ 513: 2036,
+ 514: 5448,
+ 259: 6,
+ 296: 2,
+ 282: 180.0,
+ 283: 180.0,
+ }
+
def test_exif_ifd(self):
with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif()
@@ -840,6 +851,31 @@ class TestImage:
34665: 196,
}
+ def test_exif_hide_offsets(self):
+ with Image.open("Tests/images/flower.jpg") as im:
+ exif = im.getexif()
+
+ # Check offsets are present initially
+ assert 0x8769 in exif
+ for tag in (0xA005, 0x927C):
+ assert tag in exif.get_ifd(0x8769)
+ assert exif.get_ifd(0xA005)
+ loaded_exif = exif
+
+ with Image.open("Tests/images/flower.jpg") as im:
+ new_exif = im.getexif()
+
+ for exif in (loaded_exif, new_exif):
+ exif.hide_offsets()
+
+ # Assert they are hidden afterwards,
+ # but that the IFDs are still available
+ assert 0x8769 not in exif
+ assert exif.get_ifd(0x8769)
+ for tag in (0xA005, 0x927C):
+ assert tag not in exif.get_ifd(0x8769)
+ assert exif.get_ifd(0xA005)
+
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size):
im = Image.new("RGB", size)
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index bb09a7708..6c4f1ceec 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -4,11 +4,10 @@ import sys
import sysconfig
import pytest
-from setuptools.command.build_ext import new_compiler
from PIL import Image
-from .helper import assert_image_equal, hopper, is_win32, on_ci
+from .helper import assert_image_equal, hopper, is_win32
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
@@ -131,8 +130,7 @@ class TestImageGetPixel(AccessTest):
bands = Image.getmodebands(mode)
if bands == 1:
return 1
- else:
- return tuple(range(1, bands + 1))
+ return tuple(range(1, bands + 1))
def check(self, mode, c=None):
if not c:
@@ -345,13 +343,14 @@ class TestCffi(AccessTest):
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode):
- for color in [(255, 0, 0), (255, 0, 0, 127)]:
+ for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
access = PyAccess.new(im, False)
access.putpixel((0, 0), color)
- alpha = color[3] if len(color) == 4 and mode == "PA" else 255
- assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
+ if len(color) == 3:
+ color += (255,)
+ assert im.convert("RGBA").getpixel((0, 0)) == color
class TestImagePutPixelError(AccessTest):
@@ -406,15 +405,14 @@ class TestImagePutPixelError(AccessTest):
class TestEmbeddable:
- @pytest.mark.skipif(
- not is_win32() or on_ci(),
- reason="Failing on AppVeyor / GitHub Actions when run from subprocess, "
- "not from shell",
- )
+ @pytest.mark.xfail(reason="failing test")
+ @pytest.mark.skipif(not is_win32(), reason="requires Windows")
def test_embeddable(self):
import ctypes
- with open("embed_pil.c", "w") as fh:
+ from setuptools.command.build_ext import new_compiler
+
+ with open("embed_pil.c", "w", encoding="utf-8") as fh:
fh.write(
"""
#include "Python.h"
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index 7e5fd6fe1..ae3518e44 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -35,10 +35,13 @@ def test_toarray():
test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8)
- if parse_version(numpy.__version__) >= parse_version("1.23"):
- with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
+ with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
+ if parse_version(numpy.__version__) >= parse_version("1.23"):
with pytest.raises(OSError):
numpy.array(im_truncated)
+ else:
+ with pytest.warns(UserWarning):
+ numpy.array(im_truncated)
def test_fromarray():
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 1a78f8b4c..0a7202a33 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -38,6 +38,12 @@ def test_sanity():
convert(im, output_mode)
+def test_unsupported_conversion():
+ im = hopper()
+ with pytest.raises(ValueError):
+ im.convert("INVALID")
+
+
def test_default():
im = hopper("P")
@@ -98,6 +104,13 @@ def test_rgba_p():
assert_image_similar(im, comparable, 20)
+def test_rgba():
+ with Image.open("Tests/images/transparent.png") as im:
+ assert im.mode == "RGBA"
+
+ assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5)
+
+
def test_trns_p(tmp_path):
im = hopper("P")
im.info["transparency"] = 0
@@ -242,6 +255,17 @@ def test_p2pa_palette():
assert im_pa.getpalette() == im.getpalette()
+@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
+def test_rgb_lab(mode):
+ im = Image.new(mode, (1, 1))
+ converted_im = im.convert("LAB")
+ assert converted_im.getpixel((0, 0)) == (0, 128, 128)
+
+ im = Image.new("LAB", (1, 1), (255, 0, 0))
+ converted_im = im.convert(mode)
+ assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
+
+
def test_matrix_illegal_conversion():
# Arrange
im = hopper("CMYK")
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index 3d60e52a2..0e6293349 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -55,10 +55,11 @@ def test_mode_with_L_with_float():
assert im.getpixel((0, 0)) == 2
-def test_mode_i():
+@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
+def test_mode_i(mode):
src = hopper("L")
data = list(src.getdata())
- im = Image.new("I", src.size, 0)
+ im = Image.new(mode, src.size, 0)
im.putdata(data, 2, 256)
target = [2 * elt + 256 for elt in data]
diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py
index 801161511..ae8d740a0 100644
--- a/Tests/test_image_reduce.py
+++ b/Tests/test_image_reduce.py
@@ -196,11 +196,11 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255):
)
-def test_mode_L():
+@pytest.mark.parametrize("factor", remarkable_factors)
+def test_mode_L(factor):
im = get_image("L")
- for factor in remarkable_factors:
- compare_reduce_with_reference(im, factor)
- compare_reduce_with_box(im, factor)
+ compare_reduce_with_reference(im, factor)
+ compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors)
diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py
index 5ce98a235..53ceb6df0 100644
--- a/Tests/test_image_resample.py
+++ b/Tests/test_image_resample.py
@@ -554,44 +554,48 @@ class TestCoreResampleBox:
# check that the difference at least that much
assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}")
- def test_skip_horizontal(self):
+ @pytest.mark.parametrize(
+ "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
+ )
+ def test_skip_horizontal(self, flt):
# Can skip resize for one dimension
im = hopper()
- for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
- for size, box in [
- ((40, 50), (0, 0, 40, 90)),
- ((40, 50), (0, 20, 40, 90)),
- ((40, 50), (10, 0, 50, 90)),
- ((40, 50), (10, 20, 50, 90)),
- ]:
- res = im.resize(size, flt, box)
- assert res.size == size
- # Borders should be slightly different
- assert_image_similar(
- res,
- im.crop(box).resize(size, flt),
- 0.4,
- f">>> {size} {box} {flt}",
- )
+ for size, box in [
+ ((40, 50), (0, 0, 40, 90)),
+ ((40, 50), (0, 20, 40, 90)),
+ ((40, 50), (10, 0, 50, 90)),
+ ((40, 50), (10, 20, 50, 90)),
+ ]:
+ res = im.resize(size, flt, box)
+ assert res.size == size
+ # Borders should be slightly different
+ assert_image_similar(
+ res,
+ im.crop(box).resize(size, flt),
+ 0.4,
+ f">>> {size} {box} {flt}",
+ )
- def test_skip_vertical(self):
+ @pytest.mark.parametrize(
+ "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
+ )
+ def test_skip_vertical(self, flt):
# Can skip resize for one dimension
im = hopper()
- for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]:
- for size, box in [
- ((40, 50), (0, 0, 90, 50)),
- ((40, 50), (20, 0, 90, 50)),
- ((40, 50), (0, 10, 90, 60)),
- ((40, 50), (20, 10, 90, 60)),
- ]:
- res = im.resize(size, flt, box)
- assert res.size == size
- # Borders should be slightly different
- assert_image_similar(
- res,
- im.crop(box).resize(size, flt),
- 0.4,
- f">>> {size} {box} {flt}",
- )
+ for size, box in [
+ ((40, 50), (0, 0, 90, 50)),
+ ((40, 50), (20, 0, 90, 50)),
+ ((40, 50), (0, 10, 90, 60)),
+ ((40, 50), (20, 10, 90, 60)),
+ ]:
+ res = im.resize(size, flt, box)
+ assert res.size == size
+ # Borders should be slightly different
+ assert_image_similar(
+ res,
+ im.crop(box).resize(size, flt),
+ 0.4,
+ f">>> {size} {box} {flt}",
+ )
diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py
index fbed276b8..5cb7c9a8b 100644
--- a/Tests/test_image_split.py
+++ b/Tests/test_image_split.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image, features
from .helper import assert_image_equal, hopper
@@ -29,19 +31,12 @@ def test_split():
assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)]
-def test_split_merge():
- def split_merge(mode):
- return Image.merge(mode, hopper(mode).split())
-
- assert_image_equal(hopper("1"), split_merge("1"))
- assert_image_equal(hopper("L"), split_merge("L"))
- assert_image_equal(hopper("I"), split_merge("I"))
- assert_image_equal(hopper("F"), split_merge("F"))
- assert_image_equal(hopper("P"), split_merge("P"))
- assert_image_equal(hopper("RGB"), split_merge("RGB"))
- assert_image_equal(hopper("RGBA"), split_merge("RGBA"))
- assert_image_equal(hopper("CMYK"), split_merge("CMYK"))
- assert_image_equal(hopper("YCbCr"), split_merge("YCbCr"))
+@pytest.mark.parametrize(
+ "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr")
+)
+def test_split_merge(mode):
+ expected = Image.merge(mode, hopper(mode).split())
+ assert_image_equal(hopper(mode), expected)
def test_split_open(tmp_path):
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index d1dd1e47c..4c4c41b7b 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -64,7 +64,9 @@ def test_mode_mismatch():
ImageDraw.ImageDraw(im, mode="L")
-def helper_arc(bbox, start, end):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
+def test_arc(bbox, start, end):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -76,16 +78,6 @@ def helper_arc(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1)
-def test_arc1():
- helper_arc(BBOX1, 0, 180)
- helper_arc(BBOX1, 0.5, 180.4)
-
-
-def test_arc2():
- helper_arc(BBOX2, 0, 180)
- helper_arc(BBOX2, 0.5, 180.4)
-
-
def test_arc_end_le_start():
# Arrange
im = Image.new("RGB", (W, H))
@@ -192,29 +184,21 @@ def test_bitmap():
assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png")
-def helper_chord(mode, bbox, start, end):
+@pytest.mark.parametrize("mode", ("RGB", "L"))
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_chord(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
expected = f"Tests/images/imagedraw_chord_{mode}.png"
# Act
- draw.chord(bbox, start, end, fill="red", outline="yellow")
+ draw.chord(bbox, 0, 180, fill="red", outline="yellow")
# Assert
assert_image_similar_tofile(im, expected, 1)
-def test_chord1():
- for mode in ["RGB", "L"]:
- helper_chord(mode, BBOX1, 0, 180)
-
-
-def test_chord2():
- for mode in ["RGB", "L"]:
- helper_chord(mode, BBOX2, 0, 180)
-
-
def test_chord_width():
# Arrange
im = Image.new("RGB", (W, H))
@@ -263,7 +247,9 @@ def test_chord_too_fat():
assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png")
-def helper_ellipse(mode, bbox):
+@pytest.mark.parametrize("mode", ("RGB", "L"))
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_ellipse(mode, bbox):
# Arrange
im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im)
@@ -276,16 +262,6 @@ def helper_ellipse(mode, bbox):
assert_image_similar_tofile(im, expected, 1)
-def test_ellipse1():
- for mode in ["RGB", "L"]:
- helper_ellipse(mode, BBOX1)
-
-
-def test_ellipse2():
- for mode in ["RGB", "L"]:
- helper_ellipse(mode, BBOX2)
-
-
def test_ellipse_translucent():
# Arrange
im = Image.new("RGB", (W, H))
@@ -405,7 +381,8 @@ def test_ellipse_various_sizes_filled():
)
-def helper_line(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -417,14 +394,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def test_line1():
- helper_line(POINTS1)
-
-
-def test_line2():
- helper_line(POINTS2)
-
-
def test_shape1():
# Arrange
im = Image.new("RGB", (100, 100), "white")
@@ -484,7 +453,9 @@ def test_transform():
assert_image_equal(im, expected)
-def helper_pieslice(bbox, start, end):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
+def test_pieslice(bbox, start, end):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -496,16 +467,6 @@ def helper_pieslice(bbox, start, end):
assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1)
-def test_pieslice1():
- helper_pieslice(BBOX1, -92, 46)
- helper_pieslice(BBOX1, -92.2, 46.2)
-
-
-def test_pieslice2():
- helper_pieslice(BBOX2, -92, 46)
- helper_pieslice(BBOX2, -92.2, 46.2)
-
-
def test_pieslice_width():
# Arrange
im = Image.new("RGB", (W, H))
@@ -585,7 +546,8 @@ def test_pieslice_no_spikes():
assert_image_equal(im, im_pre_erase)
-def helper_point(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_point(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -597,15 +559,8 @@ def helper_point(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
-def test_point1():
- helper_point(POINTS1)
-
-
-def test_point2():
- helper_point(POINTS2)
-
-
-def helper_polygon(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -617,14 +572,6 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
-def test_polygon1():
- helper_polygon(POINTS1)
-
-
-def test_polygon2():
- helper_polygon(POINTS2)
-
-
@pytest.mark.parametrize("mode", ("RGB", "L"))
def test_polygon_kite(mode):
# Test drawing lines of different gradients (dx>dy, dy>dx) and
@@ -682,7 +629,8 @@ def test_polygon_translucent():
assert_image_equal_tofile(im, expected)
-def helper_rectangle(bbox):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
@@ -694,14 +642,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
-def test_rectangle1():
- helper_rectangle(BBOX1)
-
-
-def test_rectangle2():
- helper_rectangle(BBOX2)
-
-
def test_big_rectangle():
# Test drawing a rectangle bigger than the image
# Arrange
@@ -1298,6 +1238,27 @@ def test_stroke_descender():
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
+@skip_unless_feature("freetype2")
+def test_split_word():
+ # Arrange
+ im = Image.new("RGB", (230, 55))
+ expected = im.copy()
+ expected_draw = ImageDraw.Draw(expected)
+ font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 48)
+ expected_draw.text((0, 0), "paradise", font=font)
+
+ draw = ImageDraw.Draw(im)
+
+ # Act
+ draw.text((0, 0), "par", font=font)
+
+ length = draw.textlength("par", font=font)
+ draw.text((length, 0), "adise", font=font)
+
+ # Assert
+ assert_image_equal(im, expected)
+
+
@skip_unless_feature("freetype2")
def test_stroke_multiline():
# Arrange
@@ -1503,7 +1464,7 @@ def test_discontiguous_corners_polygon():
assert_image_similar_tofile(img, expected, 1)
-def test_polygon():
+def test_polygon2():
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py
index e4e8a38cb..6fc829f1a 100644
--- a/Tests/test_imagedraw2.py
+++ b/Tests/test_imagedraw2.py
@@ -52,27 +52,19 @@ def test_sanity():
draw.line(list(range(10)), pen)
-def helper_ellipse(mode, bbox):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_ellipse(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
pen = ImageDraw2.Pen("blue", width=2)
brush = ImageDraw2.Brush("green")
- expected = f"Tests/images/imagedraw_ellipse_{mode}.png"
# Act
draw.ellipse(bbox, pen, brush)
# Assert
- assert_image_similar_tofile(im, expected, 1)
-
-
-def test_ellipse1():
- helper_ellipse("RGB", BBOX1)
-
-
-def test_ellipse2():
- helper_ellipse("RGB", BBOX2)
+ assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1)
def test_ellipse_edge():
@@ -88,7 +80,8 @@ def test_ellipse_edge():
assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1)
-def helper_line(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_line(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -101,14 +94,6 @@ def helper_line(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def test_line1_pen():
- helper_line(POINTS1)
-
-
-def test_line2_pen():
- helper_line(POINTS2)
-
-
def test_line_pen_as_brush():
# Arrange
im = Image.new("RGB", (W, H))
@@ -124,7 +109,8 @@ def test_line_pen_as_brush():
assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png")
-def helper_polygon(points):
+@pytest.mark.parametrize("points", (POINTS1, POINTS2))
+def test_polygon(points):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -138,15 +124,8 @@ def helper_polygon(points):
assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png")
-def test_polygon1():
- helper_polygon(POINTS1)
-
-
-def test_polygon2():
- helper_polygon(POINTS2)
-
-
-def helper_rectangle(bbox):
+@pytest.mark.parametrize("bbox", (BBOX1, BBOX2))
+def test_rectangle(bbox):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw2.Draw(im)
@@ -160,14 +139,6 @@ def helper_rectangle(bbox):
assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png")
-def test_rectangle1():
- helper_rectangle(BBOX1)
-
-
-def test_rectangle2():
- helper_rectangle(BBOX2)
-
-
def test_big_rectangle():
# Test drawing a rectangle bigger than the image
# Arrange
diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py
index 8bc94401e..221ef8cdb 100644
--- a/Tests/test_imageenhance.py
+++ b/Tests/test_imageenhance.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image, ImageEnhance
from .helper import assert_image_equal, hopper
@@ -39,17 +41,17 @@ def _check_alpha(im, original, op, amount):
)
-def test_alpha():
+@pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness"))
+def test_alpha(op):
# Issue https://github.com/python-pillow/Pillow/issues/899
# Is alpha preserved through image enhancement?
original = _half_transparent_image()
- for op in ["Color", "Brightness", "Contrast", "Sharpness"]:
- for amount in [0, 0.5, 1.0]:
- _check_alpha(
- getattr(ImageEnhance, op)(original).enhance(amount),
- original,
- op,
- amount,
- )
+ for amount in [0, 0.5, 1.0]:
+ _check_alpha(
+ getattr(ImageEnhance, op)(original).enhance(amount),
+ original,
+ op,
+ amount,
+ )
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 09e5370e2..306a2f1bf 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -632,24 +632,24 @@ def test_imagefont_getters(font):
assert len(log) == 11
-def test_getsize_stroke(font):
- for stroke_width in [0, 2]:
- assert font.getbbox("A", stroke_width=stroke_width) == (
- 0 - stroke_width,
- 4 - stroke_width,
- 12 + stroke_width,
- 16 + stroke_width,
+@pytest.mark.parametrize("stroke_width", (0, 2))
+def test_getsize_stroke(font, stroke_width):
+ assert font.getbbox("A", stroke_width=stroke_width) == (
+ 0 - stroke_width,
+ 4 - stroke_width,
+ 12 + stroke_width,
+ 16 + stroke_width,
+ )
+ with pytest.warns(DeprecationWarning) as log:
+ assert font.getsize("A", stroke_width=stroke_width) == (
+ 12 + stroke_width * 2,
+ 16 + stroke_width * 2,
)
- with pytest.warns(DeprecationWarning) as log:
- assert font.getsize("A", stroke_width=stroke_width) == (
- 12 + stroke_width * 2,
- 16 + stroke_width * 2,
- )
- assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == (
- 48 + stroke_width * 2,
- 36 + stroke_width * 4,
- )
- assert len(log) == 2
+ assert font.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == (
+ 48 + stroke_width * 2,
+ 36 + stroke_width * 4,
+ )
+ assert len(log) == 2
def test_complex_font_settings():
@@ -746,12 +746,14 @@ def test_variation_set_by_name(font):
_check_text(font, "Tests/images/variation_adobe.png", 11)
for name in ["Bold", b"Bold"]:
font.set_variation_by_name(name)
- _check_text(font, "Tests/images/variation_adobe_name.png", 11)
+ assert font.getname()[1] == "Bold"
+ _check_text(font, "Tests/images/variation_adobe_name.png", 16)
font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
_check_text(font, "Tests/images/variation_tiny.png", 40)
for name in ["200", b"200"]:
font.set_variation_by_name(name)
+ assert font.getname()[1] == "200"
_check_text(font, "Tests/images/variation_tiny_name.png", 40)
@@ -935,7 +937,30 @@ def test_standard_embedded_color(layout_engine):
d = ImageDraw.Draw(im)
d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True)
- assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2)
+ assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)
+
+
+@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
+def test_float_coord(layout_engine, fontmode):
+ txt = "Hello World!"
+ ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
+
+ im = Image.new("RGB", (300, 64), "white")
+ d = ImageDraw.Draw(im)
+ if fontmode == "1":
+ d.fontmode = "1"
+
+ embedded_color = fontmode == "RGBA"
+ d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color)
+ try:
+ assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9)
+ except AssertionError:
+ if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC:
+ assert_image_similar_tofile(
+ im, "Tests/images/text_float_coord_1_alt.png", 1
+ )
+ else:
+ raise
def test_cbdt(layout_engine):
@@ -1040,6 +1065,25 @@ def test_colr_mask(layout_engine):
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
+def test_woff2(layout_engine):
+ try:
+ font = ImageFont.truetype(
+ "Tests/fonts/OpenSans.woff2",
+ size=64,
+ layout_engine=layout_engine,
+ )
+ except OSError as e:
+ assert str(e) in ("unimplemented feature", "unknown file format")
+ pytest.skip("FreeType compiled without brotli or WOFF2 support")
+
+ im = Image.new("RGB", (350, 100), "white")
+ d = ImageDraw.Draw(im)
+
+ d.text((15, 5), "OpenSans", "black", font=font)
+
+ assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5)
+
+
def test_fill_deprecation(font):
with pytest.warns(DeprecationWarning):
font.getmask2("Hello world", fill=Image.core.fill)
diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py
index fa2291582..317db4c01 100644
--- a/Tests/test_imagegrab.py
+++ b/Tests/test_imagegrab.py
@@ -1,4 +1,5 @@
import os
+import shutil
import subprocess
import sys
@@ -33,7 +34,9 @@ class TestImageGrab:
@pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB")
def test_grab_no_xcb(self):
- if sys.platform not in ("win32", "darwin"):
+ if sys.platform not in ("win32", "darwin") and not shutil.which(
+ "gnome-screenshot"
+ ):
with pytest.raises(OSError) as e:
ImageGrab.grab()
assert str(e.value).startswith("Pillow was built without XCB support")
@@ -61,9 +64,13 @@ $bmp = New-Object Drawing.Bitmap 200, 200
)
p.communicate()
else:
- with pytest.raises(NotImplementedError) as e:
- ImageGrab.grabclipboard()
- assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only"
+ if not shutil.which("wl-paste"):
+ with pytest.raises(
+ NotImplementedError,
+ match="wl-paste or xclip is required for"
+ r" ImageGrab.grabclipboard\(\) on Linux",
+ ):
+ ImageGrab.grabclipboard()
return
ImageGrab.grabclipboard()
diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py
index 39d91eade..fe7ac9a7a 100644
--- a/Tests/test_imagemath.py
+++ b/Tests/test_imagemath.py
@@ -6,10 +6,8 @@ from PIL import Image, ImageMath
def pixel(im):
if hasattr(im, "im"):
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
- else:
- if isinstance(im, int):
- return int(im) # hack to deal with booleans
- print(im)
+ if isinstance(im, int):
+ return int(im) # hack to deal with booleans
A = Image.new("L", (1, 1), 1)
diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py
index 6de953068..29c71f917 100644
--- a/Tests/test_imagemorph.py
+++ b/Tests/test_imagemorph.py
@@ -65,14 +65,16 @@ def create_lut():
# create_lut()
-def test_lut():
- for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"):
- lb = ImageMorph.LutBuilder(op_name=op)
- assert lb.get_lut() is None
+@pytest.mark.parametrize(
+ "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge")
+)
+def test_lut(op):
+ lb = ImageMorph.LutBuilder(op_name=op)
+ assert lb.get_lut() is None
- lut = lb.build_lut()
- with open(f"Tests/images/{op}.lut", "rb") as f:
- assert lut == bytearray(f.read())
+ lut = lb.build_lut()
+ with open(f"Tests/images/{op}.lut", "rb") as f:
+ assert lut == bytearray(f.read())
def test_no_operator_loaded():
diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py
index 475d249ed..5bda28117 100644
--- a/Tests/test_imagepalette.py
+++ b/Tests/test_imagepalette.py
@@ -50,6 +50,16 @@ def test_getcolor():
palette.getcolor("unknown")
+def test_getcolor_rgba_color_rgb_palette():
+ palette = ImagePalette.ImagePalette("RGB")
+
+ # Opaque RGBA colors are converted
+ assert palette.getcolor((0, 0, 0, 255)) == palette.getcolor((0, 0, 0))
+
+ with pytest.raises(ValueError):
+ palette.getcolor((0, 0, 0, 128))
+
+
@pytest.mark.parametrize(
"index, palette",
[
diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py
index 55d7c9479..3e147a9ef 100644
--- a/Tests/test_imageshow.py
+++ b/Tests/test_imageshow.py
@@ -45,10 +45,10 @@ def test_viewer_show(order):
not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs",
)
-def test_show():
- for mode in ("1", "I;16", "LA", "RGB", "RGBA"):
- im = hopper(mode)
- assert ImageShow.show(im)
+@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
+def test_show(mode):
+ im = hopper(mode)
+ assert ImageShow.show(im)
def test_show_without_viewers():
@@ -70,12 +70,12 @@ def test_viewer():
viewer.get_command(None)
-def test_viewers():
- for viewer in ImageShow._viewers:
- try:
- viewer.get_command("test.jpg")
- except NotImplementedError:
- pass
+@pytest.mark.parametrize("viewer", ImageShow._viewers)
+def test_viewers(viewer):
+ try:
+ viewer.get_command("test.jpg")
+ except NotImplementedError:
+ pass
def test_ipythonviewer():
@@ -95,14 +95,14 @@ def test_ipythonviewer():
not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs",
)
-def test_file_deprecated(tmp_path):
+@pytest.mark.parametrize("viewer", ImageShow._viewers)
+def test_file_deprecated(tmp_path, viewer):
f = str(tmp_path / "temp.jpg")
- for viewer in ImageShow._viewers:
- hopper().save(f)
- with pytest.warns(DeprecationWarning):
- try:
- viewer.show_file(file=f)
- except NotImplementedError:
- pass
- with pytest.raises(TypeError):
- viewer.show_file()
+ hopper().save(f)
+ with pytest.warns(DeprecationWarning):
+ try:
+ viewer.show_file(file=f)
+ except NotImplementedError:
+ pass
+ with pytest.raises(TypeError):
+ viewer.show_file()
diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py
index a848c786f..995d0ee1f 100644
--- a/Tests/test_imagetk.py
+++ b/Tests/test_imagetk.py
@@ -54,19 +54,19 @@ def test_kw():
assert im is None
-def test_photoimage():
- for mode in TK_MODES:
- # test as image:
- im = hopper(mode)
+@pytest.mark.parametrize("mode", TK_MODES)
+def test_photoimage(mode):
+ # test as image:
+ im = hopper(mode)
- # this should not crash
- im_tk = ImageTk.PhotoImage(im)
+ # this should not crash
+ im_tk = ImageTk.PhotoImage(im)
- assert im_tk.width() == im.width
- assert im_tk.height() == im.height
+ assert im_tk.width() == im.width
+ assert im_tk.height() == im.height
- reloaded = ImageTk.getimage(im_tk)
- assert_image_equal(reloaded, im.convert("RGBA"))
+ reloaded = ImageTk.getimage(im_tk)
+ assert_image_equal(reloaded, im.convert("RGBA"))
def test_photoimage_apply_transparency():
@@ -76,17 +76,17 @@ def test_photoimage_apply_transparency():
assert_image_equal(reloaded, im.convert("RGBA"))
-def test_photoimage_blank():
+@pytest.mark.parametrize("mode", TK_MODES)
+def test_photoimage_blank(mode):
# test a image using mode/size:
- for mode in TK_MODES:
- im_tk = ImageTk.PhotoImage(mode, (100, 100))
+ im_tk = ImageTk.PhotoImage(mode, (100, 100))
- assert im_tk.width() == 100
- assert im_tk.height() == 100
+ assert im_tk.width() == 100
+ assert im_tk.height() == 100
- im = Image.new(mode, (100, 100))
- reloaded = ImageTk.getimage(im_tk)
- assert_image_equal(reloaded.convert(mode), im)
+ im = Image.new(mode, (100, 100))
+ reloaded = ImageTk.getimage(im_tk)
+ assert_image_equal(reloaded.convert(mode), im)
def test_box_deprecation():
diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py
index 6e8a2ac58..efcdab9ec 100644
--- a/Tests/test_mode_i16.py
+++ b/Tests/test_mode_i16.py
@@ -1,3 +1,5 @@
+import pytest
+
from PIL import Image
from .helper import hopper
@@ -20,65 +22,56 @@ def verify(im1):
), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}"
-def test_basic(tmp_path):
+@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I"))
+def test_basic(tmp_path, mode):
# PIL 1.1 has limited support for 16-bit image data. Check that
# create/copy/transform and save works as expected.
- def basic(mode):
+ im_in = original.convert(mode)
+ verify(im_in)
- im_in = original.convert(mode)
- verify(im_in)
+ w, h = im_in.size
- w, h = im_in.size
+ im_out = im_in.copy()
+ verify(im_out) # copy
- im_out = im_in.copy()
- verify(im_out) # copy
+ im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
+ verify(im_out) # transform
- im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
- verify(im_out) # transform
+ filename = str(tmp_path / "temp.im")
+ im_in.save(filename)
- filename = str(tmp_path / "temp.im")
- im_in.save(filename)
-
- with Image.open(filename) as im_out:
-
- verify(im_in)
- verify(im_out)
-
- im_out = im_in.crop((0, 0, w, h))
- verify(im_out)
-
- im_out = Image.new(mode, (w, h), None)
- im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
- im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
+ with Image.open(filename) as im_out:
verify(im_in)
verify(im_out)
- im_in = Image.new(mode, (1, 1), 1)
- assert im_in.getpixel((0, 0)) == 1
+ im_out = im_in.crop((0, 0, w, h))
+ verify(im_out)
- im_in.putpixel((0, 0), 2)
- assert im_in.getpixel((0, 0)) == 2
+ im_out = Image.new(mode, (w, h), None)
+ im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
+ im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
- if mode == "L":
- maximum = 255
- else:
- maximum = 32767
+ verify(im_in)
+ verify(im_out)
- im_in = Image.new(mode, (1, 1), 256)
- assert im_in.getpixel((0, 0)) == min(256, maximum)
+ im_in = Image.new(mode, (1, 1), 1)
+ assert im_in.getpixel((0, 0)) == 1
- im_in.putpixel((0, 0), 512)
- assert im_in.getpixel((0, 0)) == min(512, maximum)
+ im_in.putpixel((0, 0), 2)
+ assert im_in.getpixel((0, 0)) == 2
- basic("L")
+ if mode == "L":
+ maximum = 255
+ else:
+ maximum = 32767
- basic("I;16")
- basic("I;16B")
- basic("I;16L")
+ im_in = Image.new(mode, (1, 1), 256)
+ assert im_in.getpixel((0, 0)) == min(256, maximum)
- basic("I")
+ im_in.putpixel((0, 0), 512)
+ assert im_in.getpixel((0, 0)) == min(512, maximum)
def test_tobytes():
diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py
index 9735837bc..3de7ec30f 100644
--- a/Tests/test_numpy.py
+++ b/Tests/test_numpy.py
@@ -34,7 +34,7 @@ def test_numpy_to_image():
# Check supported 1-bit integer formats
assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE)
- assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE)
+ assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE)
# Check supported 8-bit integer formats
assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE)
@@ -137,19 +137,9 @@ def test_save_tiff_uint16():
assert img_px[0, 0] == pixel_value
-def test_to_array():
- def _to_array(mode, dtype):
- img = hopper(mode)
-
- # Resize to non-square
- img = img.crop((3, 0, 124, 127))
- assert img.size == (121, 127)
-
- np_img = numpy.array(img)
- _test_img_equals_nparray(img, np_img)
- assert np_img.dtype == dtype
-
- modes = [
+@pytest.mark.parametrize(
+ "mode, dtype",
+ (
("L", numpy.uint8),
("I", numpy.int32),
("F", numpy.float32),
@@ -163,10 +153,18 @@ def test_to_array():
("I;16B", ">u2"),
("I;16L", "' where is one of"
@echo " html to make standalone HTML files"
+ @echo " serve to start a local server for viewing docs"
+ @echo " livehtml to start a local server for viewing docs and auto-reload on change"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@@ -39,42 +40,49 @@ help:
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
+.PHONY: clean
clean:
-rm -rf $(BUILDDIR)/*
install-sphinx:
- $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile
+ $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph
+.PHONY: html
html:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+.PHONY: dirhtml
dirhtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+.PHONY: singlehtml
singlehtml:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+.PHONY: pickle
pickle:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
+.PHONY: json
json:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
+.PHONY: htmlhelp
htmlhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@@ -82,6 +90,7 @@ htmlhelp:
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
+.PHONY: qthelp
qthelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@@ -92,6 +101,7 @@ qthelp:
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
+.PHONY: devhelp
devhelp:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@@ -102,12 +112,14 @@ devhelp:
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork"
@echo "# devhelp"
+.PHONY: epub
epub:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+.PHONY: latex
latex:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@@ -116,6 +128,7 @@ latex:
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
+.PHONY: latexpdf
latexpdf:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@@ -123,18 +136,21 @@ latexpdf:
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+.PHONY: text
text:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
+.PHONY: man
man:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+.PHONY: texinfo
texinfo:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@@ -143,6 +159,7 @@ texinfo:
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
+.PHONY: info
info:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@@ -150,18 +167,21 @@ info:
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+.PHONY: gettext
gettext:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+.PHONY: changes
changes:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
+.PHONY: linkcheck
linkcheck:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto
@@ -169,14 +189,17 @@ linkcheck:
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
+.PHONY: doctest
doctest:
$(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
+.PHONY: livehtml
livehtml: html
livereload $(BUILDDIR)/html -p 33233
+.PHONY: serve
serve:
cd $(BUILDDIR)/html; $(PYTHON) -m http.server
diff --git a/docs/conf.py b/docs/conf.py
index bc67d9368..04823e2d7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -27,12 +27,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_copybutton",
+ "sphinx_inline_tabs",
+ "sphinx_issues",
+ "sphinx_removed_in",
"sphinxext.opengraph",
]
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index ec3938b36..26451533e 100644
--- a/docs/example/DdsImagePlugin.py
+++ b/docs/example/DdsImagePlugin.py
@@ -211,13 +211,16 @@ class DdsImageFile(ImageFile.ImageFile):
def _open(self):
if not _accept(self.fp.read(4)):
- raise SyntaxError("not a DDS file")
+ msg = "not a DDS file"
+ raise SyntaxError(msg)
(header_size,) = struct.unpack("`_ and packages
+ are tested by the ports team with all supported FreeBSD versions.
-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. Raqm support requires
-FriBiDi to be installed separately::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
-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
-FriBiDi to be installed separately::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow
-
-Most major Linux distributions, including Fedora, Ubuntu and ArchLinux
-also include Pillow in packages that previously contained PIL e.g.
-``python-imaging``. Debian splits it into two packages, ``python3-pil``
-and ``python3-pil.imagetk``.
-
-FreeBSD Installation
-^^^^^^^^^^^^^^^^^^^^
-
-Pillow can be installed on FreeBSD via the official Ports or Packages systems:
-
-**Ports**::
-
- cd /usr/ports/graphics/py-pillow && make install clean
-
-**Packages**::
-
- pkg install py38-pillow
-
-.. note::
-
- The `Pillow FreeBSD port
- `_ and packages
- are tested by the ports team with all supported FreeBSD versions.
-
+.. _Building on Linux:
+.. _Building on macOS:
+.. _Building on Windows:
+.. _Building on Windows using MSYS2/MinGW:
+.. _Building on FreeBSD:
+.. _Building on Android:
Building From Source
--------------------
-Download and extract the `compressed archive from PyPI`_.
-
-.. _compressed archive from PyPI: https://pypi.org/project/Pillow/
-
.. _external-libraries:
External Libraries
@@ -140,14 +143,14 @@ 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.4**
+ * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5**
* **libfreetype** provides type related services
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
- above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**.
+ above uses liblcms2. Tested with **1.19** and **2.7-2.14**.
* **libwebp** provides the WebP format.
@@ -191,7 +194,141 @@ Many of Pillow's features require external libraries:
* **libxcb** provides X11 screengrab support.
-Once you have installed the prerequisites, run::
+.. tab:: Linux
+
+ If you didn't build Python from source, make sure you have Python's
+ development libraries installed.
+
+ In Debian or Ubuntu::
+
+ sudo apt-get install python3-dev python3-setuptools
+
+ In Fedora, the command is::
+
+ sudo dnf install python3-devel redhat-rpm-config
+
+ In Alpine, the command is::
+
+ sudo apk add python3-dev py3-setuptools
+
+ .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
+
+ Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
+
+ sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
+ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
+ libharfbuzz-dev libfribidi-dev libxcb1-dev
+
+ To install libraqm, ``sudo apt-get install meson`` and then see
+ ``depends/install_raqm.sh``.
+
+ Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
+
+ sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
+ freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
+ harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
+
+ Note that the package manager may be yum or DNF, depending on the
+ exact distribution.
+
+ Prerequisites are installed for **Alpine** with::
+
+ sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
+ libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
+ libxcb-dev libpng-dev
+
+ See also the ``Dockerfile``\s in the Test Infrastructure repo
+ (https://github.com/python-pillow/docker-images) for a known working
+ install process for other tested distros.
+
+.. tab:: macOS
+
+ The Xcode command line tools are required to compile portions of
+ Pillow. The tools are installed by running ``xcode-select --install``
+ from the command line. The command line tools are required even if you
+ have the full Xcode package installed. It may be necessary to run
+ ``sudo xcodebuild -license`` to accept the license prior to using the
+ tools.
+
+ The easiest way to install external libraries is via `Homebrew
+ `_. After you install Homebrew, run::
+
+ brew install libjpeg libtiff little-cms2 openjpeg webp
+
+ To install libraqm on macOS use Homebrew to install its dependencies::
+
+ brew install freetype harfbuzz fribidi
+
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+
+.. tab:: Windows
+
+ We recommend you use prebuilt wheels from PyPI.
+ If you wish to compile Pillow manually, you can use the build scripts
+ in the ``winbuild`` directory used for CI testing and development.
+ These scripts require Visual Studio 2017 or newer and NASM.
+
+ The scripts also install Pillow from the local copy of the source code, so the
+ `Installing`_ instructions will not be necessary afterwards.
+
+.. tab:: Windows using MSYS2/MinGW
+
+ To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
+ **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
+
+ The following instructions target the 64-bit build, for 32-bit
+ replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
+
+ Make sure you have Python and GCC installed::
+
+ pacman -S \
+ mingw-w64-x86_64-gcc \
+ mingw-w64-x86_64-python3 \
+ mingw-w64-x86_64-python3-pip \
+ mingw-w64-x86_64-python3-setuptools
+
+ Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
+
+ pacman -S \
+ mingw-w64-x86_64-libjpeg-turbo \
+ mingw-w64-x86_64-zlib \
+ mingw-w64-x86_64-libtiff \
+ mingw-w64-x86_64-freetype \
+ mingw-w64-x86_64-lcms2 \
+ mingw-w64-x86_64-libwebp \
+ mingw-w64-x86_64-openjpeg2 \
+ mingw-w64-x86_64-libimagequant \
+ mingw-w64-x86_64-libraqm
+
+.. tab:: FreeBSD
+
+ .. Note:: Only FreeBSD 10 and 11 tested
+
+ Make sure you have Python's development libraries installed::
+
+ sudo pkg install python3
+
+ Prerequisites are installed on **FreeBSD 10 or 11** with::
+
+ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
+
+ Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
+
+.. tab:: Android
+
+ Basic Android support has been added for compilation within the Termux
+ environment. The dependencies can be installed by::
+
+ pkg install -y python ndk-sysroot clang make \
+ libjpeg-turbo
+
+ This has been tested within the Termux app on ChromeOS, on x86.
+
+Installing
+^^^^^^^^^^
+
+Once you have installed the prerequisites, to install Pillow from the source
+code on PyPI, run::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow --no-binary :all:
@@ -211,9 +348,19 @@ prerequisites, it may be necessary to manually clear the pip cache or
build without cache using the ``--no-cache-dir`` option to force a
build with newly installed external libraries.
+If you would like to install from a local copy of the source code instead, you
+can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow``
+or download and extract the `compressed archive from PyPI`_.
+
+After navigating to the Pillow directory, run::
+
+ python3 -m pip install --upgrade pip
+ python3 -m pip install .
+
+.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files
Build Options
-^^^^^^^^^^^^^
+"""""""""""""
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY``
@@ -256,157 +403,6 @@ Sample usage::
python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]"
-
-Building on macOS
-^^^^^^^^^^^^^^^^^
-
-The Xcode command line tools are required to compile portions of
-Pillow. The tools are installed by running ``xcode-select --install``
-from the command line. The command line tools are required even if you
-have the full Xcode package installed. It may be necessary to run
-``sudo xcodebuild -license`` to accept the license prior to using the
-tools.
-
-The easiest way to install external libraries is via `Homebrew
-`_. After you install Homebrew, run::
-
- brew install libjpeg libtiff little-cms2 openjpeg webp
-
-To install libraqm on macOS use Homebrew to install its dependencies::
-
- brew install freetype harfbuzz fribidi
-
-Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
-
-Now install Pillow with::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
-
-or from within the uncompressed source directory::
-
- python3 -m pip install .
-
-Building on Windows
-^^^^^^^^^^^^^^^^^^^
-
-We recommend you use prebuilt wheels from PyPI.
-If you wish to compile Pillow manually, you can use the build scripts
-in the ``winbuild`` directory used for CI testing and development.
-These scripts require Visual Studio 2017 or newer and NASM.
-
-Building on Windows using MSYS2/MinGW
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or
-**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly.
-
-The following instructions target the 64-bit build, for 32-bit
-replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``.
-
-Make sure you have Python and GCC installed::
-
- pacman -S \
- mingw-w64-x86_64-gcc \
- mingw-w64-x86_64-python3 \
- mingw-w64-x86_64-python3-pip \
- mingw-w64-x86_64-python3-setuptools
-
-Prerequisites are installed on **MSYS2 MinGW 64-bit** with::
-
- pacman -S \
- mingw-w64-x86_64-libjpeg-turbo \
- mingw-w64-x86_64-zlib \
- mingw-w64-x86_64-libtiff \
- mingw-w64-x86_64-freetype \
- mingw-w64-x86_64-lcms2 \
- mingw-w64-x86_64-libwebp \
- mingw-w64-x86_64-openjpeg2 \
- mingw-w64-x86_64-libimagequant \
- mingw-w64-x86_64-libraqm
-
-Now install Pillow with::
-
- python3 -m pip install --upgrade pip
- python3 -m pip install --upgrade Pillow --no-binary :all:
-
-
-Building on FreeBSD
-^^^^^^^^^^^^^^^^^^^
-
-.. Note:: Only FreeBSD 10 and 11 tested
-
-Make sure you have Python's development libraries installed::
-
- sudo pkg install python3
-
-Prerequisites are installed on **FreeBSD 10 or 11** with::
-
- sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb
-
-Then see ``depends/install_raqm_cmake.sh`` to install libraqm.
-
-
-Building on Linux
-^^^^^^^^^^^^^^^^^
-
-If you didn't build Python from source, make sure you have Python's
-development libraries installed.
-
-In Debian or Ubuntu::
-
- sudo apt-get install python3-dev python3-setuptools
-
-In Fedora, the command is::
-
- sudo dnf install python3-devel redhat-rpm-config
-
-In Alpine, the command is::
-
- sudo apk add python3-dev py3-setuptools
-
-.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions.
-
-Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with::
-
- sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \
- libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \
- libharfbuzz-dev libfribidi-dev libxcb1-dev
-
-To install libraqm, ``sudo apt-get install meson`` and then see
-``depends/install_raqm.sh``.
-
-Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with::
-
- sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \
- freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \
- harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel
-
-Note that the package manager may be yum or DNF, depending on the
-exact distribution.
-
-Prerequisites are installed for **Alpine** with::
-
- sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \
- libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \
- libxcb-dev libpng-dev
-
-See also the ``Dockerfile``\s in the Test Infrastructure repo
-(https://github.com/python-pillow/docker-images) for a known working
-install process for other tested distros.
-
-Building on Android
-^^^^^^^^^^^^^^^^^^^
-
-Basic Android support has been added for compilation within the Termux
-environment. The dependencies can be installed by::
-
- pkg install -y python ndk-sysroot clang make \
- libjpeg-turbo
-
-This has been tested within the Termux app on ChromeOS, on x86.
-
-
Platform Support
----------------
@@ -440,10 +436,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+
-| Fedora 35 | 3.10 | x86-64 |
-+----------------------------------+----------------------------+---------------------+
| Fedora 36 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
+| Fedora 37 | 3.11 | x86-64 |
++----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 |
@@ -464,7 +460,7 @@ These platforms are built and tested for every change.
| +----------------------------+---------------------+
| | 3.9 (MinGW) | x86, x86-64 |
| +----------------------------+---------------------+
-| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 |
+| | 3.8, 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+
@@ -482,11 +478,13 @@ These platforms have been reported to work at the versions mentioned.
| Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors |
+==================================+===========================+==================+==============+
-| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |arm |
+| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
++----------------------------------+---------------------------+------------------+--------------+
+| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm |
+----------------------------------+---------------------------+------------------+--------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm |
| +---------------------------+------------------+--------------+
-| | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |x86-64 |
+| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 |
| +---------------------------+------------------+ |
| | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+
diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst
index 794fa238f..464ab77ea 100644
--- a/docs/reference/ExifTags.rst
+++ b/docs/reference/ExifTags.rst
@@ -4,14 +4,56 @@
:py:mod:`~PIL.ExifTags` Module
==============================
-The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which
-provide constants and clear-text names for various well-known EXIF tags.
+The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes
+which provide constants and clear-text names for various well-known EXIF tags.
+
+.. py:data:: Base
+
+ >>> from PIL.ExifTags import Base
+ >>> Base.ImageDescription.value
+ 270
+ >>> Base(270).name
+ 'ImageDescription'
+
+.. py:data:: GPS
+
+ >>> from PIL.ExifTags import GPS
+ >>> GPS.GPSDestLatitude.value
+ 20
+ >>> GPS(20).name
+ 'GPSDestLatitude'
+
+.. py:data:: Interop
+
+ >>> from PIL.ExifTags import Interop
+ >>> Interop.RelatedImageFileFormat.value
+ 4096
+ >>> Interop(4096).name
+ 'RelatedImageFileFormat'
+
+.. py:data:: IFD
+
+ >>> from PIL.ExifTags import IFD
+ >>> IFD.Exif.value
+ 34665
+ >>> IFD(34665).name
+ 'Exif
+
+.. py:data:: LightSource
+
+ >>> from PIL.ExifTags import LightSource
+ >>> LightSource.Unknown.value
+ 0
+ >>> LightSource(0).name
+ 'Unknown'
+
+Two of these values are also exposed as dictionaries.
.. py:data:: TAGS
:type: dict
The TAGS dictionary maps 16-bit integer EXIF tag enumerations to
- descriptive string names. For instance:
+ descriptive string names. For instance:
>>> from PIL.ExifTags import TAGS
>>> TAGS[0x010e]
@@ -20,8 +62,8 @@ provide constants and clear-text names for various well-known EXIF tags.
.. py:data:: GPSTAGS
:type: dict
- The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to
- descriptive string names. For instance:
+ The GPSTAGS dictionary maps 8-bit integer EXIF GPS enumerations to
+ descriptive string names. For instance:
>>> from PIL.ExifTags import GPSTAGS
>>> GPSTAGS[20]
diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst
index 25f98b767..9aa26916a 100644
--- a/docs/reference/ImageDraw.rst
+++ b/docs/reference/ImageDraw.rst
@@ -139,17 +139,50 @@ Functions
must be the same as the image mode. If omitted, the mode
defaults to the mode of the image.
+Attributes
+----------
+
+.. py:attribute:: ImageDraw.fill
+ :type: bool
+ :value: False
+
+ Selects whether :py:attr:`ImageDraw.ink` should be used as a fill or outline color.
+
+.. py:attribute:: ImageDraw.font
+
+ The current default font.
+
+ Can be set per instance::
+
+ from PIL import ImageDraw, ImageFont
+ draw = ImageDraw.Draw(image)
+ draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
+ Or globally for all future ImageDraw instances::
+
+ from PIL import ImageDraw, ImageFont
+ ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
+.. py:attribute:: ImageDraw.fontmode
+
+ The current font drawing mode.
+
+ Set to ``"1"`` to disable antialiasing or ``"L"`` to enable it.
+
+.. py:attribute:: ImageDraw.ink
+ :type: int
+
+ The internal representation of the current default color.
+
Methods
-------
.. py:method:: ImageDraw.getfont()
- Get the current default font.
+ Get the current default font, :py:attr:`ImageDraw.font`.
- To set the default font for all future ImageDraw instances::
-
- from PIL import ImageDraw, ImageFont
- ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+ If the current default font is ``None``,
+ it is initialized with :py:func:`.ImageFont.load_default`.
:returns: An image font.
diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst
index 57646e558..48ce6fef7 100644
--- a/docs/releasenotes/9.1.0.rst
+++ b/docs/releasenotes/9.1.0.rst
@@ -202,7 +202,7 @@ Pillow now builds binary wheels for musllinux, suitable for Linux distributions
(rather than the glibc library used by manylinux wheels). See :pep:`656`.
ImageShow temporary files on Unix
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`,
a temporary file is created from the image. On Unix, Pillow will no longer delete these
diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst
index 7109a09f2..fde2faae3 100644
--- a/docs/releasenotes/9.3.0.rst
+++ b/docs/releasenotes/9.3.0.rst
@@ -1,28 +1,6 @@
9.3.0
-----
-Backwards Incompatible Changes
-==============================
-
-TODO
-^^^^
-
-Deprecations
-============
-
-TODO
-^^^^
-
-TODO
-
-API Changes
-===========
-
-TODO
-^^^^
-
-TODO
-
API Additions
=============
@@ -51,19 +29,79 @@ Additional images can also be appended when saving, by combining the
im.save(out, save_all=True, append_images=[im1, im2, ...])
+Added ExifTags enums
+^^^^^^^^^^^^^^^^^^^^
+
+The data from :py:data:`~PIL.ExifTags.TAGS` and
+:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum``
+classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`.
+
Security
========
-TODO
-^^^^
+Initialize libtiff buffer when saving
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When saving a TIFF image to a file object using libtiff, the buffer was not
+initialized. This behaviour introduced in Pillow 2.0.0, and has now been fixed.
+
+Decode JPEG compressed BLP1 data in original mode
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Within the BLP image format, BLP1 data may use JPEG compression. Instead of
+telling the JPEG library that this data is in BGRX mode, Pillow will now
+decode the data in its natural CMYK mode, then convert it to RGB and rearrange
+the channels afterwards. Trying to load the data in an incorrect mode could
+result in a segmentation fault. This issue was introduced in Pillow 9.1.0.
+
+Limit SAMPLESPERPIXEL to avoid runtime DOS
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A large value in the ``SAMPLESPERPIXEL`` tag could lead to a memory and runtime DOS in
+``TiffImagePlugin.py`` when setting up the context for image decoding.
+This was introduced in Pillow 9.2.0, found with `OSS-Fuzz`_ and fixed by limiting
+``SAMPLESPERPIXEL`` to the number of planes that we can decode.
-TODO
Other Changes
=============
-Added DDS ATI1 and ATI2 reading
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Python 3.11 wheels
+^^^^^^^^^^^^^^^^^^
-Support has been added to read the ATI1 and ATI2 formats of DDS images.
+Pillow 9.2.0 had wheels built against Python 3.11 beta, available as a preview to help
+others prepare for 3.11, and ensure Pillow can be used immediately on release day of
+3.11.0 final (2022-10-24, :pep:`664`).
+
+Pillow 9.3.0 now officially includes binary wheels for Python 3.11 final.
+
+Windows wheels
+^^^^^^^^^^^^^^
+
+This release contains wheels for Windows built using GitHub Actions.
+
+Previously they were built by `Christoph Gohlke `_.
+
+A huge thanks to Christoph for building Windows binaries for us for around a decade,
+plus testing, and fixing over a hundred bug fixes along the way, in addition to building
+and hosting unofficial Windows binaries for hundreds of Python projects!
+
+Added DDS ATI1, ATI2 and BC6H reading
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images.
+
+Release GIL when converting images using matrix operations
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Python's Global Interpreter Lock is now released when converting images using matrix
+operations.
+
+Show all frames with ImageShow
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When calling :py:meth:`~PIL.Image.Image.show` or using
+:py:mod:`~PIL.ImageShow`, all frames will now be shown.
+
+.. _OSS-Fuzz: https://github.com/google/oss-fuzz
diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst
new file mode 100644
index 000000000..e4e1e40fe
--- /dev/null
+++ b/docs/releasenotes/9.4.0.rst
@@ -0,0 +1,111 @@
+9.4.0
+-----
+
+Backwards Incompatible Changes
+==============================
+
+TODO
+^^^^
+
+TODO
+
+Deprecations
+============
+
+TODO
+^^^^
+
+TODO
+
+API Changes
+===========
+
+TODO
+^^^^
+
+TODO
+
+API Additions
+=============
+
+Added start position for getmask and getmask2
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Text may render differently when starting at fractional coordinates, so
+:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now
+support a ``start`` argument. This tuple of horizontal and vertical offset
+will be used internally by :py:meth:`.ImageDraw.text` to more accurately place
+text at the ``xy`` coordinates.
+
+Added the ``exact`` encoding option for WebP
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``exact`` encoding option for WebP is now supported. The WebP encoder
+removes the hidden RGB values for better compression by default in libwebp 0.5
+or later. By setting this option to ``True``, the encoder will keep the hidden
+RGB values.
+
+Added ``signed`` option when saving JPEG2000
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If the ``signed`` keyword argument is present and true when saving JPEG2000
+images, then tell the encoder to save the image as signed.
+
+Added IFD, Interop and LightSource ExifTags enums
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with
+:py:meth:`~PIL.Image.Exif.get_ifd`::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/flower.jpg")
+ print(im.getexif().get_ifd(ExifTags.IFD.Exif))
+
+``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should
+not be used in other contexts, as the enum value is only internally meaningful.
+
+:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/flower.jpg")
+ interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop)
+ print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98
+
+:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource
+tag::
+
+ from PIL import Image, ExifTags
+ im = Image.open("Tests/images/iptc.jpg")
+ exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif)
+ print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown
+
+getxmp()
+^^^^^^^^
+
+`XMP data `_ can now be
+decoded for WEBP images through ``getxmp()``.
+
+Writing JPEG comments
+^^^^^^^^^^^^^^^^^^^^^
+
+When saving a JPEG image, a comment can now be written from
+:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving::
+
+ im.save(out, comment="Test comment")
+
+Security
+========
+
+TODO
+^^^^
+
+TODO
+
+Other Changes
+=============
+
+Added support for DDS L and LA images
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Support has been added to read and write L and LA DDS images in the uncompressed
+format, known as "luminance" textures.
diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst
index 8c436be3b..a2b588696 100644
--- a/docs/releasenotes/index.rst
+++ b/docs/releasenotes/index.rst
@@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2
+ 9.4.0
9.3.0
9.2.0
9.1.1
diff --git a/setup.cfg b/setup.cfg
index 44feb25ff..b562e2934 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -46,6 +46,7 @@ docs =
olefile
sphinx>=2.4
sphinx-copybutton
+ sphinx-inline-tabs
sphinx-issues>=3.0.1
sphinx-removed-in
sphinxext-opengraph
diff --git a/setup.py b/setup.py
index aa3168aa5..243365681 100755
--- a/setup.py
+++ b/setup.py
@@ -15,15 +15,13 @@ import subprocess
import sys
import warnings
-from setuptools import Extension
-from setuptools import __version__ as setuptools_version
-from setuptools import setup
+from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
def get_version():
version_file = "src/PIL/_version.py"
- with open(version_file) as f:
+ with open(version_file, encoding="utf-8") as f:
exec(compile(f.read(), version_file, "exec"))
return locals()["__version__"]
@@ -364,15 +362,15 @@ class pil_build_ext(build_ext):
self.feature.required.discard(x)
_dbg("Disabling %s", x)
if getattr(self, f"enable_{x}"):
- raise ValueError(
- f"Conflicting options: --enable-{x} and --disable-{x}"
- )
+ msg = f"Conflicting options: --enable-{x} and --disable-{x}"
+ raise ValueError(msg)
if x == "freetype":
_dbg("--disable-freetype implies --disable-raqm")
if getattr(self, "enable_raqm"):
- raise ValueError(
+ msg = (
"Conflicting options: --enable-raqm and --disable-freetype"
)
+ raise ValueError(msg)
setattr(self, "disable_raqm", True)
if getattr(self, f"enable_{x}"):
_dbg("Requiring %s", x)
@@ -383,13 +381,11 @@ class pil_build_ext(build_ext):
for x in ("raqm", "fribidi"):
if getattr(self, f"vendor_{x}"):
if getattr(self, "disable_raqm"):
- raise ValueError(
- f"Conflicting options: --vendor-{x} and --disable-raqm"
- )
+ msg = f"Conflicting options: --vendor-{x} and --disable-raqm"
+ raise ValueError(msg)
if x == "fribidi" and not getattr(self, "vendor_raqm"):
- raise ValueError(
- f"Conflicting options: --vendor-{x} and not --vendor-raqm"
- )
+ msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm"
+ raise ValueError(msg)
_dbg("Using vendored version of %s", x)
self.feature.vendor.add(x)
@@ -852,7 +848,6 @@ class pil_build_ext(build_ext):
sys.platform == "win32"
and sys.version_info < (3, 9)
and not (PLATFORM_PYPY or PLATFORM_MINGW)
- and int(setuptools_version.split(".")[0]) < 60
):
defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""'))
else:
diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py
index 102b72e1d..e0dd4dede 100644
--- a/src/PIL/BdfFontFile.py
+++ b/src/PIL/BdfFontFile.py
@@ -86,7 +86,8 @@ class BdfFontFile(FontFile.FontFile):
s = fp.readline()
if s[:13] != b"STARTFONT 2.1":
- raise SyntaxError("not a valid BDF file")
+ msg = "not a valid BDF file"
+ raise SyntaxError(msg)
props = {}
comments = []
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 104fbada9..1cc0d4b3c 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -65,7 +65,8 @@ def __getattr__(name):
if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
def unpack_565(i):
@@ -278,7 +279,8 @@ class BlpImageFile(ImageFile.ImageFile):
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
- raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}")
+ msg = f"Bad BLP magic {repr(self.magic)}"
+ raise BLPFormatError(msg)
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
@@ -292,7 +294,8 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._read_blp_header()
self._load()
except struct.error as e:
- raise OSError("Truncated BLP file") from e
+ msg = "Truncated BLP file"
+ raise OSError(msg) from e
return -1, 0
def _read_blp_header(self):
@@ -354,13 +357,11 @@ class BLP1Decoder(_BLPBaseDecoder):
data = self._read_bgra(palette)
self.set_as_raw(bytes(data))
else:
- raise BLPFormatError(
- f"Unsupported BLP encoding {repr(self._blp_encoding)}"
- )
+ msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
+ raise BLPFormatError(msg)
else:
- raise BLPFormatError(
- f"Unsupported BLP compression {repr(self._blp_encoding)}"
- )
+ msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
+ raise BLPFormatError(msg)
def _decode_jpeg_stream(self):
from .JpegImagePlugin import JpegImageFile
@@ -373,8 +374,11 @@ class BLP1Decoder(_BLPBaseDecoder):
data = BytesIO(data)
image = JpegImageFile(data)
Image._decompression_bomb_check(image.size)
- image.mode = "RGB"
- image.tile = [("jpeg", (0, 0) + self.size, 0, ("BGRX", ""))]
+ if image.mode == "CMYK":
+ decoder_name, extents, offset, args = image.tile[0]
+ image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
+ r, g, b = image.convert("RGB").split()
+ image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes())
@@ -412,16 +416,15 @@ class BLP2Decoder(_BLPBaseDecoder):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
- raise BLPFormatError(
- f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
- )
+ msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
+ raise BLPFormatError(msg)
else:
- raise BLPFormatError(f"Unknown BLP encoding {repr(self._blp_encoding)}")
+ msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
+ raise BLPFormatError(msg)
else:
- raise BLPFormatError(
- f"Unknown BLP compression {repr(self._blp_compression)}"
- )
+ msg = f"Unknown BLP compression {repr(self._blp_compression)}"
+ raise BLPFormatError(msg)
self.set_as_raw(bytes(data))
@@ -457,7 +460,8 @@ class BLPEncoder(ImageFile.PyEncoder):
def _save(im, fp, filename, save_all=False):
if im.mode != "P":
- raise ValueError("Unsupported BLP image mode")
+ msg = "Unsupported BLP image mode"
+ raise ValueError(msg)
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index 1041ab763..e13b18f27 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -146,7 +146,8 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["a_mask"],
)
else:
- raise OSError(f"Unsupported BMP header type ({file_info['header_size']})")
+ msg = f"Unsupported BMP header type ({file_info['header_size']})"
+ raise OSError(msg)
# ------------------ Special case : header is reported 40, which
# ---------------------- is shorter than real size for bpp >= 16
@@ -164,7 +165,8 @@ class BmpImageFile(ImageFile.ImageFile):
# ---------------------- Check bit depth for unusual unsupported values
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None:
- raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
+ msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
+ raise OSError(msg)
# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
@@ -205,23 +207,27 @@ class BmpImageFile(ImageFile.ImageFile):
):
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else:
- raise OSError("Unsupported BMP bitfields layout")
+ msg = "Unsupported BMP bitfields layout"
+ raise OSError(msg)
else:
- raise OSError("Unsupported BMP bitfields layout")
+ msg = "Unsupported BMP bitfields layout"
+ raise OSError(msg)
elif file_info["compression"] == self.RAW:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self.mode = "BGRA", "RGBA"
- elif file_info["compression"] == self.RLE8:
+ elif file_info["compression"] in (self.RLE8, self.RLE4):
decoder_name = "bmp_rle"
else:
- raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
+ msg = f"Unsupported BMP compression ({file_info['compression']})"
+ raise OSError(msg)
# --------------- Once the header is processed, process the palette/LUT
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
# ---------------------------------------------------- 1-bit images
if not (0 < file_info["colors"] <= 65536):
- raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})")
+ msg = f"Unsupported BMP Palette size ({file_info['colors']})"
+ raise OSError(msg)
else:
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
@@ -250,16 +256,18 @@ class BmpImageFile(ImageFile.ImageFile):
# ---------------------------- Finally set the tile data for the plugin
self.info["compression"] = file_info["compression"]
+ args = [raw_mode]
+ if decoder_name == "bmp_rle":
+ args.append(file_info["compression"] == self.RLE4)
+ else:
+ args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
+ args.append(file_info["direction"])
self.tile = [
(
decoder_name,
(0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(),
- (
- raw_mode,
- ((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3),
- file_info["direction"],
- ),
+ tuple(args),
)
]
@@ -269,7 +277,8 @@ class BmpImageFile(ImageFile.ImageFile):
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes
if not _accept(head_data):
- raise SyntaxError("Not a BMP file")
+ msg = "Not a BMP file"
+ raise SyntaxError(msg)
# read the start position of the BMP image data (u32)
offset = i32(head_data, 10)
# load bitmap information (offset=raster info)
@@ -280,6 +289,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
+ rle4 = self.args[1]
data = bytearray()
x = 0
while len(data) < self.state.xsize * self.state.ysize:
@@ -293,7 +303,16 @@ class BmpRleDecoder(ImageFile.PyDecoder):
if x + num_pixels > self.state.xsize:
# Too much data for row
num_pixels = max(0, self.state.xsize - x)
- data += byte * num_pixels
+ if rle4:
+ first_pixel = o8(byte[0] >> 4)
+ second_pixel = o8(byte[0] & 0x0F)
+ for index in range(num_pixels):
+ if index % 2 == 0:
+ data += first_pixel
+ else:
+ data += second_pixel
+ else:
+ data += byte * num_pixels
x += num_pixels
else:
if byte[0] == 0:
@@ -314,9 +333,18 @@ class BmpRleDecoder(ImageFile.PyDecoder):
x = len(data) % self.state.xsize
else:
# absolute mode
- bytes_read = self.fd.read(byte[0])
- data += bytes_read
- if len(bytes_read) < byte[0]:
+ if rle4:
+ # 2 pixels per byte
+ byte_count = byte[0] // 2
+ bytes_read = self.fd.read(byte_count)
+ for byte_read in bytes_read:
+ data += o8(byte_read >> 4)
+ data += o8(byte_read & 0x0F)
+ else:
+ byte_count = byte[0]
+ bytes_read = self.fd.read(byte_count)
+ data += bytes_read
+ if len(bytes_read) < byte_count:
break
x += byte[0]
@@ -362,7 +390,8 @@ def _save(im, fp, filename, bitmap_header=True):
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as BMP") from e
+ msg = f"cannot write mode {im.mode} as BMP"
+ raise OSError(msg) from e
info = im.encoderinfo
@@ -390,7 +419,8 @@ def _save(im, fp, filename, bitmap_header=True):
offset = 14 + header + colors * 4
file_size = offset + image
if file_size > 2**32 - 1:
- raise ValueError("File size is too large for the BMP format")
+ msg = "File size is too large for the BMP format"
+ raise ValueError(msg)
fp.write(
b"BM" # file type (magic)
+ o32(file_size) # file size
diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py
index 9510f733e..a0da1b786 100644
--- a/src/PIL/BufrStubImagePlugin.py
+++ b/src/PIL/BufrStubImagePlugin.py
@@ -42,7 +42,8 @@ class BufrStubImageFile(ImageFile.StubImageFile):
offset = self.fp.tell()
if not _accept(self.fp.read(4)):
- raise SyntaxError("Not a BUFR file")
+ msg = "Not a BUFR file"
+ raise SyntaxError(msg)
self.fp.seek(offset)
@@ -60,7 +61,8 @@ class BufrStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("BUFR save handler not installed")
+ msg = "BUFR save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py
index 42af5cafc..aedc6ce7f 100644
--- a/src/PIL/CurImagePlugin.py
+++ b/src/PIL/CurImagePlugin.py
@@ -43,7 +43,8 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
# check magic
s = self.fp.read(6)
if not _accept(s):
- raise SyntaxError("not a CUR file")
+ msg = "not a CUR file"
+ raise SyntaxError(msg)
# pick the largest cursor in the file
m = b""
@@ -54,7 +55,8 @@ class CurImageFile(BmpImagePlugin.BmpImageFile):
elif s[0] > m[0] and s[1] > m[1]:
m = s
if not m:
- raise TypeError("No cursors were found")
+ msg = "No cursors were found"
+ raise TypeError(msg)
# load as bitmap
self._bitmap(i32(m, 12) + offset)
diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py
index aeed1e7c7..81c0314f0 100644
--- a/src/PIL/DcxImagePlugin.py
+++ b/src/PIL/DcxImagePlugin.py
@@ -47,7 +47,8 @@ class DcxImageFile(PcxImageFile):
# Header
s = self.fp.read(4)
if not _accept(s):
- raise SyntaxError("not a DCX file")
+ msg = "not a DCX file"
+ raise SyntaxError(msg)
# Component directory
self._offset = []
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index bba480161..a946daeaa 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -101,6 +101,8 @@ DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
DXGI_FORMAT_BC5_TYPELESS = 82
DXGI_FORMAT_BC5_UNORM = 83
DXGI_FORMAT_BC5_SNORM = 84
+DXGI_FORMAT_BC6H_UF16 = 95
+DXGI_FORMAT_BC6H_SF16 = 96
DXGI_FORMAT_BC7_TYPELESS = 97
DXGI_FORMAT_BC7_UNORM = 98
DXGI_FORMAT_BC7_UNORM_SRGB = 99
@@ -112,13 +114,16 @@ class DdsImageFile(ImageFile.ImageFile):
def _open(self):
if not _accept(self.fp.read(4)):
- raise SyntaxError("not a DDS file")
+ msg = "not a DDS file"
+ raise SyntaxError(msg)
(header_size,) = struct.unpack(" 255:
- raise SyntaxError("not an EPS file")
+ msg = "not an EPS file"
+ raise SyntaxError(msg)
try:
m = split.match(s)
except re.error as e:
- raise SyntaxError("not an EPS file") from e
+ msg = "not an EPS file"
+ raise SyntaxError(msg) from e
if m:
k, v = m.group(1, 2)
@@ -268,7 +271,8 @@ class EpsImageFile(ImageFile.ImageFile):
# tools mistakenly put in the Comments section
pass
else:
- raise OSError("bad EPS header")
+ msg = "bad EPS header"
+ raise OSError(msg)
s_raw = fp.readline()
s = s_raw.strip("\r\n")
@@ -282,7 +286,8 @@ class EpsImageFile(ImageFile.ImageFile):
while s[:1] == "%":
if len(s) > 255:
- raise SyntaxError("not an EPS file")
+ msg = "not an EPS file"
+ raise SyntaxError(msg)
if s[:11] == "%ImageData:":
# Encoded bitmapped image.
@@ -306,7 +311,8 @@ class EpsImageFile(ImageFile.ImageFile):
break
if not box:
- raise OSError("cannot determine EPS bounding box")
+ msg = "cannot determine EPS bounding box"
+ raise OSError(msg)
def _find_offset(self, fp):
@@ -326,7 +332,8 @@ class EpsImageFile(ImageFile.ImageFile):
offset = i32(s, 4)
length = i32(s, 8)
else:
- raise SyntaxError("not an EPS file")
+ msg = "not an EPS file"
+ raise SyntaxError(msg)
return length, offset
@@ -365,7 +372,8 @@ def _save(im, fp, filename, eps=1):
elif im.mode == "CMYK":
operator = (8, 4, b"false 4 colorimage")
else:
- raise ValueError("image mode is not supported")
+ msg = "image mode is not supported"
+ raise ValueError(msg)
if eps:
#
diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py
index 7da2ddae5..2347c6d4c 100644
--- a/src/PIL/ExifTags.py
+++ b/src/PIL/ExifTags.py
@@ -14,318 +14,367 @@ This module provides constants and clear-text names for various
well-known EXIF tags.
"""
+from enum import IntEnum
-TAGS = {
+
+class Base(IntEnum):
# possibly incomplete
- 0x0001: "InteropIndex",
- 0x000B: "ProcessingSoftware",
- 0x00FE: "NewSubfileType",
- 0x00FF: "SubfileType",
- 0x0100: "ImageWidth",
- 0x0101: "ImageLength",
- 0x0102: "BitsPerSample",
- 0x0103: "Compression",
- 0x0106: "PhotometricInterpretation",
- 0x0107: "Thresholding",
- 0x0108: "CellWidth",
- 0x0109: "CellLength",
- 0x010A: "FillOrder",
- 0x010D: "DocumentName",
- 0x010E: "ImageDescription",
- 0x010F: "Make",
- 0x0110: "Model",
- 0x0111: "StripOffsets",
- 0x0112: "Orientation",
- 0x0115: "SamplesPerPixel",
- 0x0116: "RowsPerStrip",
- 0x0117: "StripByteCounts",
- 0x0118: "MinSampleValue",
- 0x0119: "MaxSampleValue",
- 0x011A: "XResolution",
- 0x011B: "YResolution",
- 0x011C: "PlanarConfiguration",
- 0x011D: "PageName",
- 0x0120: "FreeOffsets",
- 0x0121: "FreeByteCounts",
- 0x0122: "GrayResponseUnit",
- 0x0123: "GrayResponseCurve",
- 0x0124: "T4Options",
- 0x0125: "T6Options",
- 0x0128: "ResolutionUnit",
- 0x0129: "PageNumber",
- 0x012D: "TransferFunction",
- 0x0131: "Software",
- 0x0132: "DateTime",
- 0x013B: "Artist",
- 0x013C: "HostComputer",
- 0x013D: "Predictor",
- 0x013E: "WhitePoint",
- 0x013F: "PrimaryChromaticities",
- 0x0140: "ColorMap",
- 0x0141: "HalftoneHints",
- 0x0142: "TileWidth",
- 0x0143: "TileLength",
- 0x0144: "TileOffsets",
- 0x0145: "TileByteCounts",
- 0x014A: "SubIFDs",
- 0x014C: "InkSet",
- 0x014D: "InkNames",
- 0x014E: "NumberOfInks",
- 0x0150: "DotRange",
- 0x0151: "TargetPrinter",
- 0x0152: "ExtraSamples",
- 0x0153: "SampleFormat",
- 0x0154: "SMinSampleValue",
- 0x0155: "SMaxSampleValue",
- 0x0156: "TransferRange",
- 0x0157: "ClipPath",
- 0x0158: "XClipPathUnits",
- 0x0159: "YClipPathUnits",
- 0x015A: "Indexed",
- 0x015B: "JPEGTables",
- 0x015F: "OPIProxy",
- 0x0200: "JPEGProc",
- 0x0201: "JpegIFOffset",
- 0x0202: "JpegIFByteCount",
- 0x0203: "JpegRestartInterval",
- 0x0205: "JpegLosslessPredictors",
- 0x0206: "JpegPointTransforms",
- 0x0207: "JpegQTables",
- 0x0208: "JpegDCTables",
- 0x0209: "JpegACTables",
- 0x0211: "YCbCrCoefficients",
- 0x0212: "YCbCrSubSampling",
- 0x0213: "YCbCrPositioning",
- 0x0214: "ReferenceBlackWhite",
- 0x02BC: "XMLPacket",
- 0x1000: "RelatedImageFileFormat",
- 0x1001: "RelatedImageWidth",
- 0x1002: "RelatedImageLength",
- 0x4746: "Rating",
- 0x4749: "RatingPercent",
- 0x800D: "ImageID",
- 0x828D: "CFARepeatPatternDim",
- 0x828E: "CFAPattern",
- 0x828F: "BatteryLevel",
- 0x8298: "Copyright",
- 0x829A: "ExposureTime",
- 0x829D: "FNumber",
- 0x83BB: "IPTCNAA",
- 0x8649: "ImageResources",
- 0x8769: "ExifOffset",
- 0x8773: "InterColorProfile",
- 0x8822: "ExposureProgram",
- 0x8824: "SpectralSensitivity",
- 0x8825: "GPSInfo",
- 0x8827: "ISOSpeedRatings",
- 0x8828: "OECF",
- 0x8829: "Interlace",
- 0x882A: "TimeZoneOffset",
- 0x882B: "SelfTimerMode",
- 0x8830: "SensitivityType",
- 0x8831: "StandardOutputSensitivity",
- 0x8832: "RecommendedExposureIndex",
- 0x8833: "ISOSpeed",
- 0x8834: "ISOSpeedLatitudeyyy",
- 0x8835: "ISOSpeedLatitudezzz",
- 0x9000: "ExifVersion",
- 0x9003: "DateTimeOriginal",
- 0x9004: "DateTimeDigitized",
- 0x9010: "OffsetTime",
- 0x9011: "OffsetTimeOriginal",
- 0x9012: "OffsetTimeDigitized",
- 0x9101: "ComponentsConfiguration",
- 0x9102: "CompressedBitsPerPixel",
- 0x9201: "ShutterSpeedValue",
- 0x9202: "ApertureValue",
- 0x9203: "BrightnessValue",
- 0x9204: "ExposureBiasValue",
- 0x9205: "MaxApertureValue",
- 0x9206: "SubjectDistance",
- 0x9207: "MeteringMode",
- 0x9208: "LightSource",
- 0x9209: "Flash",
- 0x920A: "FocalLength",
- 0x920B: "FlashEnergy",
+ InteropIndex = 0x0001
+ ProcessingSoftware = 0x000B
+ NewSubfileType = 0x00FE
+ SubfileType = 0x00FF
+ ImageWidth = 0x0100
+ ImageLength = 0x0101
+ BitsPerSample = 0x0102
+ Compression = 0x0103
+ PhotometricInterpretation = 0x0106
+ Thresholding = 0x0107
+ CellWidth = 0x0108
+ CellLength = 0x0109
+ FillOrder = 0x010A
+ DocumentName = 0x010D
+ ImageDescription = 0x010E
+ Make = 0x010F
+ Model = 0x0110
+ StripOffsets = 0x0111
+ Orientation = 0x0112
+ SamplesPerPixel = 0x0115
+ RowsPerStrip = 0x0116
+ StripByteCounts = 0x0117
+ MinSampleValue = 0x0118
+ MaxSampleValue = 0x0119
+ XResolution = 0x011A
+ YResolution = 0x011B
+ PlanarConfiguration = 0x011C
+ PageName = 0x011D
+ FreeOffsets = 0x0120
+ FreeByteCounts = 0x0121
+ GrayResponseUnit = 0x0122
+ GrayResponseCurve = 0x0123
+ T4Options = 0x0124
+ T6Options = 0x0125
+ ResolutionUnit = 0x0128
+ PageNumber = 0x0129
+ TransferFunction = 0x012D
+ Software = 0x0131
+ DateTime = 0x0132
+ Artist = 0x013B
+ HostComputer = 0x013C
+ Predictor = 0x013D
+ WhitePoint = 0x013E
+ PrimaryChromaticities = 0x013F
+ ColorMap = 0x0140
+ HalftoneHints = 0x0141
+ TileWidth = 0x0142
+ TileLength = 0x0143
+ TileOffsets = 0x0144
+ TileByteCounts = 0x0145
+ SubIFDs = 0x014A
+ InkSet = 0x014C
+ InkNames = 0x014D
+ NumberOfInks = 0x014E
+ DotRange = 0x0150
+ TargetPrinter = 0x0151
+ ExtraSamples = 0x0152
+ SampleFormat = 0x0153
+ SMinSampleValue = 0x0154
+ SMaxSampleValue = 0x0155
+ TransferRange = 0x0156
+ ClipPath = 0x0157
+ XClipPathUnits = 0x0158
+ YClipPathUnits = 0x0159
+ Indexed = 0x015A
+ JPEGTables = 0x015B
+ OPIProxy = 0x015F
+ JPEGProc = 0x0200
+ JpegIFOffset = 0x0201
+ JpegIFByteCount = 0x0202
+ JpegRestartInterval = 0x0203
+ JpegLosslessPredictors = 0x0205
+ JpegPointTransforms = 0x0206
+ JpegQTables = 0x0207
+ JpegDCTables = 0x0208
+ JpegACTables = 0x0209
+ YCbCrCoefficients = 0x0211
+ YCbCrSubSampling = 0x0212
+ YCbCrPositioning = 0x0213
+ ReferenceBlackWhite = 0x0214
+ XMLPacket = 0x02BC
+ RelatedImageFileFormat = 0x1000
+ RelatedImageWidth = 0x1001
+ RelatedImageLength = 0x1002
+ Rating = 0x4746
+ RatingPercent = 0x4749
+ ImageID = 0x800D
+ CFARepeatPatternDim = 0x828D
+ BatteryLevel = 0x828F
+ Copyright = 0x8298
+ ExposureTime = 0x829A
+ FNumber = 0x829D
+ IPTCNAA = 0x83BB
+ ImageResources = 0x8649
+ ExifOffset = 0x8769
+ InterColorProfile = 0x8773
+ ExposureProgram = 0x8822
+ SpectralSensitivity = 0x8824
+ GPSInfo = 0x8825
+ ISOSpeedRatings = 0x8827
+ OECF = 0x8828
+ Interlace = 0x8829
+ TimeZoneOffset = 0x882A
+ SelfTimerMode = 0x882B
+ SensitivityType = 0x8830
+ StandardOutputSensitivity = 0x8831
+ RecommendedExposureIndex = 0x8832
+ ISOSpeed = 0x8833
+ ISOSpeedLatitudeyyy = 0x8834
+ ISOSpeedLatitudezzz = 0x8835
+ ExifVersion = 0x9000
+ DateTimeOriginal = 0x9003
+ DateTimeDigitized = 0x9004
+ OffsetTime = 0x9010
+ OffsetTimeOriginal = 0x9011
+ OffsetTimeDigitized = 0x9012
+ ComponentsConfiguration = 0x9101
+ CompressedBitsPerPixel = 0x9102
+ ShutterSpeedValue = 0x9201
+ ApertureValue = 0x9202
+ BrightnessValue = 0x9203
+ ExposureBiasValue = 0x9204
+ MaxApertureValue = 0x9205
+ SubjectDistance = 0x9206
+ MeteringMode = 0x9207
+ LightSource = 0x9208
+ Flash = 0x9209
+ FocalLength = 0x920A
+ Noise = 0x920D
+ ImageNumber = 0x9211
+ SecurityClassification = 0x9212
+ ImageHistory = 0x9213
+ TIFFEPStandardID = 0x9216
+ MakerNote = 0x927C
+ UserComment = 0x9286
+ SubsecTime = 0x9290
+ SubsecTimeOriginal = 0x9291
+ SubsecTimeDigitized = 0x9292
+ AmbientTemperature = 0x9400
+ Humidity = 0x9401
+ Pressure = 0x9402
+ WaterDepth = 0x9403
+ Acceleration = 0x9404
+ CameraElevationAngle = 0x9405
+ XPTitle = 0x9C9B
+ XPComment = 0x9C9C
+ XPAuthor = 0x9C9D
+ XPKeywords = 0x9C9E
+ XPSubject = 0x9C9F
+ FlashPixVersion = 0xA000
+ ColorSpace = 0xA001
+ ExifImageWidth = 0xA002
+ ExifImageHeight = 0xA003
+ RelatedSoundFile = 0xA004
+ ExifInteroperabilityOffset = 0xA005
+ FlashEnergy = 0xA20B
+ SpatialFrequencyResponse = 0xA20C
+ FocalPlaneXResolution = 0xA20E
+ FocalPlaneYResolution = 0xA20F
+ FocalPlaneResolutionUnit = 0xA210
+ SubjectLocation = 0xA214
+ ExposureIndex = 0xA215
+ SensingMethod = 0xA217
+ FileSource = 0xA300
+ SceneType = 0xA301
+ CFAPattern = 0xA302
+ CustomRendered = 0xA401
+ ExposureMode = 0xA402
+ WhiteBalance = 0xA403
+ DigitalZoomRatio = 0xA404
+ FocalLengthIn35mmFilm = 0xA405
+ SceneCaptureType = 0xA406
+ GainControl = 0xA407
+ Contrast = 0xA408
+ Saturation = 0xA409
+ Sharpness = 0xA40A
+ DeviceSettingDescription = 0xA40B
+ SubjectDistanceRange = 0xA40C
+ ImageUniqueID = 0xA420
+ CameraOwnerName = 0xA430
+ BodySerialNumber = 0xA431
+ LensSpecification = 0xA432
+ LensMake = 0xA433
+ LensModel = 0xA434
+ LensSerialNumber = 0xA435
+ CompositeImage = 0xA460
+ CompositeImageCount = 0xA461
+ CompositeImageExposureTimes = 0xA462
+ Gamma = 0xA500
+ PrintImageMatching = 0xC4A5
+ DNGVersion = 0xC612
+ DNGBackwardVersion = 0xC613
+ UniqueCameraModel = 0xC614
+ LocalizedCameraModel = 0xC615
+ CFAPlaneColor = 0xC616
+ CFALayout = 0xC617
+ LinearizationTable = 0xC618
+ BlackLevelRepeatDim = 0xC619
+ BlackLevel = 0xC61A
+ BlackLevelDeltaH = 0xC61B
+ BlackLevelDeltaV = 0xC61C
+ WhiteLevel = 0xC61D
+ DefaultScale = 0xC61E
+ DefaultCropOrigin = 0xC61F
+ DefaultCropSize = 0xC620
+ ColorMatrix1 = 0xC621
+ ColorMatrix2 = 0xC622
+ CameraCalibration1 = 0xC623
+ CameraCalibration2 = 0xC624
+ ReductionMatrix1 = 0xC625
+ ReductionMatrix2 = 0xC626
+ AnalogBalance = 0xC627
+ AsShotNeutral = 0xC628
+ AsShotWhiteXY = 0xC629
+ BaselineExposure = 0xC62A
+ BaselineNoise = 0xC62B
+ BaselineSharpness = 0xC62C
+ BayerGreenSplit = 0xC62D
+ LinearResponseLimit = 0xC62E
+ CameraSerialNumber = 0xC62F
+ LensInfo = 0xC630
+ ChromaBlurRadius = 0xC631
+ AntiAliasStrength = 0xC632
+ ShadowScale = 0xC633
+ DNGPrivateData = 0xC634
+ MakerNoteSafety = 0xC635
+ CalibrationIlluminant1 = 0xC65A
+ CalibrationIlluminant2 = 0xC65B
+ BestQualityScale = 0xC65C
+ RawDataUniqueID = 0xC65D
+ OriginalRawFileName = 0xC68B
+ OriginalRawFileData = 0xC68C
+ ActiveArea = 0xC68D
+ MaskedAreas = 0xC68E
+ AsShotICCProfile = 0xC68F
+ AsShotPreProfileMatrix = 0xC690
+ CurrentICCProfile = 0xC691
+ CurrentPreProfileMatrix = 0xC692
+ ColorimetricReference = 0xC6BF
+ CameraCalibrationSignature = 0xC6F3
+ ProfileCalibrationSignature = 0xC6F4
+ AsShotProfileName = 0xC6F6
+ NoiseReductionApplied = 0xC6F7
+ ProfileName = 0xC6F8
+ ProfileHueSatMapDims = 0xC6F9
+ ProfileHueSatMapData1 = 0xC6FA
+ ProfileHueSatMapData2 = 0xC6FB
+ ProfileToneCurve = 0xC6FC
+ ProfileEmbedPolicy = 0xC6FD
+ ProfileCopyright = 0xC6FE
+ ForwardMatrix1 = 0xC714
+ ForwardMatrix2 = 0xC715
+ PreviewApplicationName = 0xC716
+ PreviewApplicationVersion = 0xC717
+ PreviewSettingsName = 0xC718
+ PreviewSettingsDigest = 0xC719
+ PreviewColorSpace = 0xC71A
+ PreviewDateTime = 0xC71B
+ RawImageDigest = 0xC71C
+ OriginalRawFileDigest = 0xC71D
+ SubTileBlockSize = 0xC71E
+ RowInterleaveFactor = 0xC71F
+ ProfileLookTableDims = 0xC725
+ ProfileLookTableData = 0xC726
+ OpcodeList1 = 0xC740
+ OpcodeList2 = 0xC741
+ OpcodeList3 = 0xC74E
+ NoiseProfile = 0xC761
+
+
+"""Maps EXIF tags to tag names."""
+TAGS = {
+ **{i.value: i.name for i in Base},
0x920C: "SpatialFrequencyResponse",
- 0x920D: "Noise",
- 0x9211: "ImageNumber",
- 0x9212: "SecurityClassification",
- 0x9213: "ImageHistory",
0x9214: "SubjectLocation",
0x9215: "ExposureIndex",
+ 0x828E: "CFAPattern",
+ 0x920B: "FlashEnergy",
0x9216: "TIFF/EPStandardID",
- 0x927C: "MakerNote",
- 0x9286: "UserComment",
- 0x9290: "SubsecTime",
- 0x9291: "SubsecTimeOriginal",
- 0x9292: "SubsecTimeDigitized",
- 0x9400: "AmbientTemperature",
- 0x9401: "Humidity",
- 0x9402: "Pressure",
- 0x9403: "WaterDepth",
- 0x9404: "Acceleration",
- 0x9405: "CameraElevationAngle",
- 0x9C9B: "XPTitle",
- 0x9C9C: "XPComment",
- 0x9C9D: "XPAuthor",
- 0x9C9E: "XPKeywords",
- 0x9C9F: "XPSubject",
- 0xA000: "FlashPixVersion",
- 0xA001: "ColorSpace",
- 0xA002: "ExifImageWidth",
- 0xA003: "ExifImageHeight",
- 0xA004: "RelatedSoundFile",
- 0xA005: "ExifInteroperabilityOffset",
- 0xA20B: "FlashEnergy",
- 0xA20C: "SpatialFrequencyResponse",
- 0xA20E: "FocalPlaneXResolution",
- 0xA20F: "FocalPlaneYResolution",
- 0xA210: "FocalPlaneResolutionUnit",
- 0xA214: "SubjectLocation",
- 0xA215: "ExposureIndex",
- 0xA217: "SensingMethod",
- 0xA300: "FileSource",
- 0xA301: "SceneType",
- 0xA302: "CFAPattern",
- 0xA401: "CustomRendered",
- 0xA402: "ExposureMode",
- 0xA403: "WhiteBalance",
- 0xA404: "DigitalZoomRatio",
- 0xA405: "FocalLengthIn35mmFilm",
- 0xA406: "SceneCaptureType",
- 0xA407: "GainControl",
- 0xA408: "Contrast",
- 0xA409: "Saturation",
- 0xA40A: "Sharpness",
- 0xA40B: "DeviceSettingDescription",
- 0xA40C: "SubjectDistanceRange",
- 0xA420: "ImageUniqueID",
- 0xA430: "CameraOwnerName",
- 0xA431: "BodySerialNumber",
- 0xA432: "LensSpecification",
- 0xA433: "LensMake",
- 0xA434: "LensModel",
- 0xA435: "LensSerialNumber",
- 0xA460: "CompositeImage",
- 0xA461: "CompositeImageCount",
- 0xA462: "CompositeImageExposureTimes",
- 0xA500: "Gamma",
- 0xC4A5: "PrintImageMatching",
- 0xC612: "DNGVersion",
- 0xC613: "DNGBackwardVersion",
- 0xC614: "UniqueCameraModel",
- 0xC615: "LocalizedCameraModel",
- 0xC616: "CFAPlaneColor",
- 0xC617: "CFALayout",
- 0xC618: "LinearizationTable",
- 0xC619: "BlackLevelRepeatDim",
- 0xC61A: "BlackLevel",
- 0xC61B: "BlackLevelDeltaH",
- 0xC61C: "BlackLevelDeltaV",
- 0xC61D: "WhiteLevel",
- 0xC61E: "DefaultScale",
- 0xC61F: "DefaultCropOrigin",
- 0xC620: "DefaultCropSize",
- 0xC621: "ColorMatrix1",
- 0xC622: "ColorMatrix2",
- 0xC623: "CameraCalibration1",
- 0xC624: "CameraCalibration2",
- 0xC625: "ReductionMatrix1",
- 0xC626: "ReductionMatrix2",
- 0xC627: "AnalogBalance",
- 0xC628: "AsShotNeutral",
- 0xC629: "AsShotWhiteXY",
- 0xC62A: "BaselineExposure",
- 0xC62B: "BaselineNoise",
- 0xC62C: "BaselineSharpness",
- 0xC62D: "BayerGreenSplit",
- 0xC62E: "LinearResponseLimit",
- 0xC62F: "CameraSerialNumber",
- 0xC630: "LensInfo",
- 0xC631: "ChromaBlurRadius",
- 0xC632: "AntiAliasStrength",
- 0xC633: "ShadowScale",
- 0xC634: "DNGPrivateData",
- 0xC635: "MakerNoteSafety",
- 0xC65A: "CalibrationIlluminant1",
- 0xC65B: "CalibrationIlluminant2",
- 0xC65C: "BestQualityScale",
- 0xC65D: "RawDataUniqueID",
- 0xC68B: "OriginalRawFileName",
- 0xC68C: "OriginalRawFileData",
- 0xC68D: "ActiveArea",
- 0xC68E: "MaskedAreas",
- 0xC68F: "AsShotICCProfile",
- 0xC690: "AsShotPreProfileMatrix",
- 0xC691: "CurrentICCProfile",
- 0xC692: "CurrentPreProfileMatrix",
- 0xC6BF: "ColorimetricReference",
- 0xC6F3: "CameraCalibrationSignature",
- 0xC6F4: "ProfileCalibrationSignature",
- 0xC6F6: "AsShotProfileName",
- 0xC6F7: "NoiseReductionApplied",
- 0xC6F8: "ProfileName",
- 0xC6F9: "ProfileHueSatMapDims",
- 0xC6FA: "ProfileHueSatMapData1",
- 0xC6FB: "ProfileHueSatMapData2",
- 0xC6FC: "ProfileToneCurve",
- 0xC6FD: "ProfileEmbedPolicy",
- 0xC6FE: "ProfileCopyright",
- 0xC714: "ForwardMatrix1",
- 0xC715: "ForwardMatrix2",
- 0xC716: "PreviewApplicationName",
- 0xC717: "PreviewApplicationVersion",
- 0xC718: "PreviewSettingsName",
- 0xC719: "PreviewSettingsDigest",
- 0xC71A: "PreviewColorSpace",
- 0xC71B: "PreviewDateTime",
- 0xC71C: "RawImageDigest",
- 0xC71D: "OriginalRawFileDigest",
- 0xC71E: "SubTileBlockSize",
- 0xC71F: "RowInterleaveFactor",
- 0xC725: "ProfileLookTableDims",
- 0xC726: "ProfileLookTableData",
- 0xC740: "OpcodeList1",
- 0xC741: "OpcodeList2",
- 0xC74E: "OpcodeList3",
- 0xC761: "NoiseProfile",
}
-"""Maps EXIF tags to tag names."""
-GPSTAGS = {
- 0: "GPSVersionID",
- 1: "GPSLatitudeRef",
- 2: "GPSLatitude",
- 3: "GPSLongitudeRef",
- 4: "GPSLongitude",
- 5: "GPSAltitudeRef",
- 6: "GPSAltitude",
- 7: "GPSTimeStamp",
- 8: "GPSSatellites",
- 9: "GPSStatus",
- 10: "GPSMeasureMode",
- 11: "GPSDOP",
- 12: "GPSSpeedRef",
- 13: "GPSSpeed",
- 14: "GPSTrackRef",
- 15: "GPSTrack",
- 16: "GPSImgDirectionRef",
- 17: "GPSImgDirection",
- 18: "GPSMapDatum",
- 19: "GPSDestLatitudeRef",
- 20: "GPSDestLatitude",
- 21: "GPSDestLongitudeRef",
- 22: "GPSDestLongitude",
- 23: "GPSDestBearingRef",
- 24: "GPSDestBearing",
- 25: "GPSDestDistanceRef",
- 26: "GPSDestDistance",
- 27: "GPSProcessingMethod",
- 28: "GPSAreaInformation",
- 29: "GPSDateStamp",
- 30: "GPSDifferential",
- 31: "GPSHPositioningError",
-}
+class GPS(IntEnum):
+ GPSVersionID = 0
+ GPSLatitudeRef = 1
+ GPSLatitude = 2
+ GPSLongitudeRef = 3
+ GPSLongitude = 4
+ GPSAltitudeRef = 5
+ GPSAltitude = 6
+ GPSTimeStamp = 7
+ GPSSatellites = 8
+ GPSStatus = 9
+ GPSMeasureMode = 10
+ GPSDOP = 11
+ GPSSpeedRef = 12
+ GPSSpeed = 13
+ GPSTrackRef = 14
+ GPSTrack = 15
+ GPSImgDirectionRef = 16
+ GPSImgDirection = 17
+ GPSMapDatum = 18
+ GPSDestLatitudeRef = 19
+ GPSDestLatitude = 20
+ GPSDestLongitudeRef = 21
+ GPSDestLongitude = 22
+ GPSDestBearingRef = 23
+ GPSDestBearing = 24
+ GPSDestDistanceRef = 25
+ GPSDestDistance = 26
+ GPSProcessingMethod = 27
+ GPSAreaInformation = 28
+ GPSDateStamp = 29
+ GPSDifferential = 30
+ GPSHPositioningError = 31
+
+
"""Maps EXIF GPS tags to tag names."""
+GPSTAGS = {i.value: i.name for i in GPS}
+
+
+class Interop(IntEnum):
+ InteropIndex = 1
+ InteropVersion = 2
+ RelatedImageFileFormat = 4096
+ RelatedImageWidth = 4097
+ RleatedImageHeight = 4098
+
+
+class IFD(IntEnum):
+ Exif = 34665
+ GPSInfo = 34853
+ Makernote = 37500
+ Interop = 40965
+ IFD1 = -1
+
+
+class LightSource(IntEnum):
+ Unknown = 0
+ Daylight = 1
+ Fluorescent = 2
+ Tungsten = 3
+ Flash = 4
+ Fine = 9
+ Cloudy = 10
+ Shade = 11
+ DaylightFluorescent = 12
+ DayWhiteFluorescent = 13
+ CoolWhiteFluorescent = 14
+ WhiteFluorescent = 15
+ StandardLightA = 17
+ StandardLightB = 18
+ StandardLightC = 19
+ D55 = 20
+ D65 = 21
+ D75 = 22
+ D50 = 23
+ ISO = 24
+ Other = 255
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index c16300efa..536bc1fe6 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -28,7 +28,8 @@ class FitsImageFile(ImageFile.ImageFile):
while True:
header = self.fp.read(80)
if not header:
- raise OSError("Truncated FITS file")
+ msg = "Truncated FITS file"
+ raise OSError(msg)
keyword = header[:8].strip()
if keyword == b"END":
break
@@ -36,12 +37,14 @@ class FitsImageFile(ImageFile.ImageFile):
if value.startswith(b"="):
value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"):
- raise SyntaxError("Not a FITS file")
+ msg = "Not a FITS file"
+ raise SyntaxError(msg)
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
- raise ValueError("No image data")
+ msg = "No image data"
+ raise ValueError(msg)
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py
index 440240a99..86eb2d5a2 100644
--- a/src/PIL/FitsStubImagePlugin.py
+++ b/src/PIL/FitsStubImagePlugin.py
@@ -67,7 +67,8 @@ class FITSStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
- raise OSError("FITS save handler not installed")
+ msg = "FITS save handler not installed"
+ raise OSError(msg)
# --------------------------------------------------------------------
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index e13b1779c..66681939d 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution.
#
+import os
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
@@ -49,7 +50,8 @@ class FliImageFile(ImageFile.ImageFile):
# HEAD
s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"):
- raise SyntaxError("not an FLI/FLC file")
+ msg = "not an FLI/FLC file"
+ raise SyntaxError(msg)
# frames
self.n_frames = i16(s, 6)
@@ -80,11 +82,19 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF1FA:
# look for palette chunk
- s = self.fp.read(6)
- if i16(s, 4) == 11:
- self._palette(palette, 2)
- elif i16(s, 4) == 4:
- self._palette(palette, 0)
+ number_of_subchunks = i16(s, 6)
+ chunk_size = None
+ for _ in range(number_of_subchunks):
+ if chunk_size is not None:
+ self.fp.seek(chunk_size - 6, os.SEEK_CUR)
+ s = self.fp.read(6)
+ chunk_type = i16(s, 4)
+ if chunk_type in (4, 11):
+ self._palette(palette, 2 if chunk_type == 11 else 0)
+ break
+ chunk_size = i32(s)
+ if not chunk_size:
+ break
palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
self.palette = ImagePalette.raw("RGB", b"".join(palette))
@@ -132,7 +142,8 @@ class FliImageFile(ImageFile.ImageFile):
self.load()
if frame != self.__frame + 1:
- raise ValueError(f"cannot seek to frame {frame}")
+ msg = f"cannot seek to frame {frame}"
+ raise ValueError(msg)
self.__frame = frame
# move to next frame
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index a55376d0e..8ddc6b40b 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -60,10 +60,12 @@ class FpxImageFile(ImageFile.ImageFile):
try:
self.ole = olefile.OleFileIO(self.fp)
except OSError as e:
- raise SyntaxError("not an FPX file; invalid OLE file") from e
+ msg = "not an FPX file; invalid OLE file"
+ raise SyntaxError(msg) from e
if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
- raise SyntaxError("not an FPX file; bad root CLSID")
+ msg = "not an FPX file; bad root CLSID"
+ raise SyntaxError(msg)
self._open_index(1)
@@ -99,7 +101,8 @@ class FpxImageFile(ImageFile.ImageFile):
colors = []
bands = i32(s, 4)
if bands > 4:
- raise OSError("Invalid number of bands")
+ msg = "Invalid number of bands"
+ raise OSError(msg)
for i in range(bands):
# note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
@@ -141,7 +144,8 @@ class FpxImageFile(ImageFile.ImageFile):
length = i32(s, 32)
if size != self.size:
- raise OSError("subimage mismatch")
+ msg = "subimage mismatch"
+ raise OSError(msg)
# get tile descriptors
fp.seek(28 + offset)
@@ -217,7 +221,8 @@ class FpxImageFile(ImageFile.ImageFile):
self.tile_prefix = self.jpeg[jpeg_tables]
else:
- raise OSError("unknown/invalid compression")
+ msg = "unknown/invalid compression"
+ raise OSError(msg)
x = x + xtile
if x >= xsize:
diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py
index 1b714eb4f..c7c32252b 100644
--- a/src/PIL/FtexImagePlugin.py
+++ b/src/PIL/FtexImagePlugin.py
@@ -73,7 +73,8 @@ def __getattr__(name):
if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
class FtexImageFile(ImageFile.ImageFile):
@@ -82,7 +83,8 @@ class FtexImageFile(ImageFile.ImageFile):
def _open(self):
if not _accept(self.fp.read(4)):
- raise SyntaxError("not an FTEX file")
+ msg = "not an FTEX file"
+ raise SyntaxError(msg)
struct.unpack(" self._max_line_size:
- raise SyntaxError("bad palette file")
+ msg = "bad palette file"
+ raise SyntaxError(msg)
# 4th column is color name and may contain spaces.
v = s.split(maxsplit=3)
if len(v) < 3:
- raise ValueError("bad palette entry")
+ msg = "bad palette entry"
+ raise ValueError(msg)
self.palette += (int(v[0]), int(v[1]), int(v[2]))
diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py
index 4575f8237..2088eb7b0 100644
--- a/src/PIL/GribStubImagePlugin.py
+++ b/src/PIL/GribStubImagePlugin.py
@@ -42,7 +42,8 @@ class GribStubImageFile(ImageFile.StubImageFile):
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
- raise SyntaxError("Not a GRIB file")
+ msg = "Not a GRIB file"
+ raise SyntaxError(msg)
self.fp.seek(offset)
@@ -60,7 +61,8 @@ class GribStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("GRIB save handler not installed")
+ msg = "GRIB save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py
index df11cf2a6..d6f283739 100644
--- a/src/PIL/Hdf5StubImagePlugin.py
+++ b/src/PIL/Hdf5StubImagePlugin.py
@@ -42,7 +42,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
- raise SyntaxError("Not an HDF file")
+ msg = "Not an HDF file"
+ raise SyntaxError(msg)
self.fp.seek(offset)
@@ -60,7 +61,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("HDF5 save handler not installed")
+ msg = "HDF5 save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
index fa192f053..e76d0c35a 100644
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -42,7 +42,8 @@ def read_32t(fobj, start_length, size):
fobj.seek(start)
sig = fobj.read(4)
if sig != b"\x00\x00\x00\x00":
- raise SyntaxError("Unknown signature, expecting 0x00000000")
+ msg = "Unknown signature, expecting 0x00000000"
+ raise SyntaxError(msg)
return read_32(fobj, (start + 4, length - 4), size)
@@ -82,7 +83,8 @@ def read_32(fobj, start_length, size):
if bytesleft <= 0:
break
if bytesleft != 0:
- raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]")
+ msg = f"Error reading channel [{repr(bytesleft)} left]"
+ raise SyntaxError(msg)
band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
im.im.putband(band.im, band_ix)
return {"RGB": im}
@@ -113,10 +115,11 @@ def read_png_or_jpeg2000(fobj, start_length, size):
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
):
if not enable_jpeg2k:
- raise ValueError(
+ msg = (
"Unsupported icon subimage format (rebuild PIL "
"with JPEG 2000 support to fix this)"
)
+ raise ValueError(msg)
# j2k, jpc or j2c
fobj.seek(start)
jp2kstream = fobj.read(length)
@@ -127,7 +130,8 @@ def read_png_or_jpeg2000(fobj, start_length, size):
im = im.convert("RGBA")
return {"RGBA": im}
else:
- raise ValueError("Unsupported icon subimage format")
+ msg = "Unsupported icon subimage format"
+ raise ValueError(msg)
class IcnsFile:
@@ -168,12 +172,14 @@ class IcnsFile:
self.fobj = fobj
sig, filesize = nextheader(fobj)
if not _accept(sig):
- raise SyntaxError("not an icns file")
+ msg = "not an icns file"
+ raise SyntaxError(msg)
i = HEADERSIZE
while i < filesize:
sig, blocksize = nextheader(fobj)
if blocksize <= 0:
- raise SyntaxError("invalid block header")
+ msg = "invalid block header"
+ raise SyntaxError(msg)
i += HEADERSIZE
blocksize -= HEADERSIZE
dct[sig] = (i, blocksize)
@@ -192,7 +198,8 @@ class IcnsFile:
def bestsize(self):
sizes = self.itersizes()
if not sizes:
- raise SyntaxError("No 32bit icon resources found")
+ msg = "No 32bit icon resources found"
+ raise SyntaxError(msg)
return max(sizes)
def dataforsize(self, size):
@@ -275,7 +282,8 @@ class IcnsImageFile(ImageFile.ImageFile):
if value in simple_sizes:
info_size = self.info["sizes"][simple_sizes.index(value)]
if info_size not in self.info["sizes"]:
- raise ValueError("This is not one of the allowed sizes of this image")
+ msg = "This is not one of the allowed sizes of this image"
+ raise ValueError(msg)
self._size = value
def load(self):
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index 17b9855a0..568e6d38d 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -127,7 +127,8 @@ class IcoFile:
# check magic
s = buf.read(6)
if not _accept(s):
- raise SyntaxError("not an ICO file")
+ msg = "not an ICO file"
+ raise SyntaxError(msg)
self.buf = buf
self.entry = []
@@ -316,7 +317,8 @@ class IcoImageFile(ImageFile.ImageFile):
@size.setter
def size(self, value):
if value not in self.info["sizes"]:
- raise ValueError("This is not one of the allowed sizes of this image")
+ msg = "This is not one of the allowed sizes of this image"
+ raise ValueError(msg)
self._size = value
def load(self):
@@ -327,6 +329,7 @@ class IcoImageFile(ImageFile.ImageFile):
# if tile is PNG, it won't really be loaded yet
im.load()
self.im = im.im
+ self.pyaccess = None
self.mode = im.mode
if im.size != self.size:
warnings.warn("Image was not the expected size")
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index 78ccfb9cf..875a20326 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -126,7 +126,8 @@ class ImImageFile(ImageFile.ImageFile):
# 100 bytes, this is (probably) not a text header.
if b"\n" not in self.fp.read(100):
- raise SyntaxError("not an IM file")
+ msg = "not an IM file"
+ raise SyntaxError(msg)
self.fp.seek(0)
n = 0
@@ -153,7 +154,8 @@ class ImImageFile(ImageFile.ImageFile):
s = s + self.fp.readline()
if len(s) > 100:
- raise SyntaxError("not an IM file")
+ msg = "not an IM file"
+ raise SyntaxError(msg)
if s[-2:] == b"\r\n":
s = s[:-2]
@@ -163,7 +165,8 @@ class ImImageFile(ImageFile.ImageFile):
try:
m = split.match(s)
except re.error as e:
- raise SyntaxError("not an IM file") from e
+ msg = "not an IM file"
+ raise SyntaxError(msg) from e
if m:
@@ -198,12 +201,12 @@ class ImImageFile(ImageFile.ImageFile):
else:
- raise SyntaxError(
- "Syntax error in IM header: " + s.decode("ascii", "replace")
- )
+ msg = "Syntax error in IM header: " + s.decode("ascii", "replace")
+ raise SyntaxError(msg)
if not n:
- raise SyntaxError("Not an IM file")
+ msg = "Not an IM file"
+ raise SyntaxError(msg)
# Basic attributes
self._size = self.info[SIZE]
@@ -213,7 +216,8 @@ class ImImageFile(ImageFile.ImageFile):
while s and s[:1] != b"\x1A":
s = self.fp.read(1)
if not s:
- raise SyntaxError("File truncated")
+ msg = "File truncated"
+ raise SyntaxError(msg)
if LUT in self.info:
# convert lookup table to palette or lut attribute
@@ -332,7 +336,8 @@ def _save(im, fp, filename):
try:
image_type, rawmode = SAVE[im.mode]
except KeyError as e:
- raise ValueError(f"Cannot save {im.mode} images as IM") from e
+ msg = f"Cannot save {im.mode} images as IM"
+ raise ValueError(msg) from e
frames = im.encoderinfo.get("frames", 1)
@@ -352,7 +357,13 @@ def _save(im, fp, filename):
fp.write(b"Lut: 1\r\n")
fp.write(b"\000" * (511 - fp.tell()) + b"\032")
if im.mode in ["P", "PA"]:
- fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes
+ im_palette = im.im.getpalette("RGB", "RGB;L")
+ colors = len(im_palette) // 3
+ palette = b""
+ for i in range(3):
+ palette += im_palette[colors * i : colors * (i + 1)]
+ palette += b"\x00" * (256 - colors)
+ fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 6611ceb3c..b22060965 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -47,7 +47,14 @@ except ImportError:
# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
# Use __version__ instead.
-from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
+from . import (
+ ExifTags,
+ ImageMode,
+ TiffTags,
+ UnidentifiedImageError,
+ __version__,
+ _plugins,
+)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._util import DeferredError, is_path
@@ -73,7 +80,8 @@ def __getattr__(name):
if name in enum.__members__:
deprecate(name, 10, f"{enum.__name__}.{name}")
return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
logger = logging.getLogger(__name__)
@@ -100,11 +108,12 @@ try:
from . import _imaging as core
if __version__ != getattr(core, "PILLOW_VERSION", None):
- raise ImportError(
+ msg = (
"The _imaging extension was built for another version of Pillow or PIL:\n"
f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n"
f"Pillow version: {__version__}"
)
+ raise ImportError(msg)
except ImportError as v:
core = DeferredError(ImportError("The _imaging C module is not installed."))
@@ -399,7 +408,8 @@ def _getdecoder(mode, decoder_name, args, extra=()):
# get decoder
decoder = getattr(core, decoder_name + "_decoder")
except AttributeError as e:
- raise OSError(f"decoder {decoder_name} not available") from e
+ msg = f"decoder {decoder_name} not available"
+ raise OSError(msg) from e
return decoder(mode, *args + extra)
@@ -422,7 +432,8 @@ def _getencoder(mode, encoder_name, args, extra=()):
# get encoder
encoder = getattr(core, encoder_name + "_encoder")
except AttributeError as e:
- raise OSError(f"encoder {encoder_name} not available") from e
+ msg = f"encoder {encoder_name} not available"
+ raise OSError(msg) from e
return encoder(mode, *args + extra)
@@ -668,7 +679,8 @@ class Image:
try:
self.save(b, "PNG")
except Exception as e:
- raise ValueError("Could not save to PNG for display") from e
+ msg = "Could not save to PNG for display"
+ raise ValueError(msg) from e
return b.getvalue()
@property
@@ -679,12 +691,24 @@ class Image:
new["shape"] = shape
new["typestr"] = typestr
new["version"] = 3
- if self.mode == "1":
- # Binary images need to be extended from bits to bytes
- # See: https://github.com/python-pillow/Pillow/issues/350
- new["data"] = self.tobytes("raw", "L")
- else:
- new["data"] = self.tobytes()
+ try:
+ if self.mode == "1":
+ # Binary images need to be extended from bits to bytes
+ # See: https://github.com/python-pillow/Pillow/issues/350
+ new["data"] = self.tobytes("raw", "L")
+ else:
+ new["data"] = self.tobytes()
+ except Exception as e:
+ if not isinstance(e, (MemoryError, RecursionError)):
+ try:
+ import numpy
+ from packaging.version import parse as parse_version
+ except ImportError:
+ pass
+ else:
+ if parse_version(numpy.__version__) < parse_version("1.23"):
+ warnings.warn(e)
+ raise
return new
def __getstate__(self):
@@ -692,7 +716,6 @@ class Image:
def __setstate__(self, state):
Image.__init__(self)
- self.tile = []
info, mode, size, palette, data = state
self.info = info
self.mode = mode
@@ -749,7 +772,8 @@ class Image:
if s:
break
if s < 0:
- raise RuntimeError(f"encoder error {s} in tobytes")
+ msg = f"encoder error {s} in tobytes"
+ raise RuntimeError(msg)
return b"".join(data)
@@ -766,7 +790,8 @@ class Image:
self.load()
if self.mode != "1":
- raise ValueError("not a bitmap")
+ msg = "not a bitmap"
+ raise ValueError(msg)
data = self.tobytes("xbm")
return b"".join(
[
@@ -800,9 +825,11 @@ class Image:
s = d.decode(data)
if s[0] >= 0:
- raise ValueError("not enough image data")
+ msg = "not enough image data"
+ raise ValueError(msg)
if s[1] != 0:
- raise ValueError("cannot decode image data")
+ msg = "cannot decode image data"
+ raise ValueError(msg)
def load(self):
"""
@@ -868,7 +895,7 @@ class Image:
and the palette can be represented without a palette.
The current version supports all possible conversions between
- "L", "RGB" and "CMYK." The ``matrix`` argument only supports "L"
+ "L", "RGB" and "CMYK". The ``matrix`` argument only supports "L"
and "RGB".
When translating a color image to greyscale (mode "L"),
@@ -887,6 +914,9 @@ class Image:
this passes the operation to :py:meth:`~PIL.Image.Image.quantize`,
and ``dither`` and ``palette`` are ignored.
+ When converting from "PA", if an "RGBA" palette is present, the alpha
+ channel from the image will be used instead of the values from the palette.
+
:param mode: The requested mode. See: :ref:`concept-modes`.
:param matrix: An optional conversion matrix. If given, this
should be 4- or 12-tuple containing floating point values.
@@ -920,7 +950,8 @@ class Image:
if matrix:
# matrix conversion
if mode not in ("L", "RGB"):
- raise ValueError("illegal conversion")
+ msg = "illegal conversion"
+ raise ValueError(msg)
im = self.im.convert_matrix(mode, matrix)
new = self._new(im)
if has_transparency and self.im.bands == 3:
@@ -1005,7 +1036,8 @@ class Image:
elif isinstance(t, int):
self.im.putpalettealpha(t, 0)
else:
- raise ValueError("Transparency for P mode should be bytes or int")
+ msg = "Transparency for P mode should be bytes or int"
+ raise ValueError(msg)
if mode == "P" and palette == Palette.ADAPTIVE:
im = self.im.quantize(colors)
@@ -1027,6 +1059,19 @@ class Image:
warnings.warn("Couldn't allocate palette entry for transparency")
return new
+ if "LAB" in (self.mode, mode):
+ other_mode = mode if self.mode == "LAB" else self.mode
+ if other_mode in ("RGB", "RGBA", "RGBX"):
+ from . import ImageCms
+
+ srgb = ImageCms.createProfile("sRGB")
+ lab = ImageCms.createProfile("LAB")
+ profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab]
+ transform = ImageCms.buildTransform(
+ profiles[0], profiles[1], self.mode, mode
+ )
+ return transform.apply(self)
+
# colorspace conversion
if dither is None:
dither = Dither.FLOYDSTEINBERG
@@ -1036,10 +1081,14 @@ class Image:
except ValueError:
try:
# normalize source image and try again
- im = self.im.convert(getmodebase(self.mode))
+ modebase = getmodebase(self.mode)
+ if modebase == self.mode:
+ raise
+ im = self.im.convert(modebase)
im = im.convert(mode, dither)
except KeyError as e:
- raise ValueError("illegal conversion") from e
+ msg = "illegal conversion"
+ raise ValueError(msg) from e
new_im = self._new(im)
if mode == "P" and palette != Palette.ADAPTIVE:
@@ -1114,20 +1163,21 @@ class Image:
Quantize.LIBIMAGEQUANT,
):
# Caller specified an invalid mode.
- raise ValueError(
+ msg = (
"Fast Octree (method == 2) and libimagequant (method == 3) "
"are the only valid methods for quantizing RGBA images"
)
+ raise ValueError(msg)
if palette:
# use palette from reference image
palette.load()
if palette.mode != "P":
- raise ValueError("bad mode for palette image")
+ msg = "bad mode for palette image"
+ raise ValueError(msg)
if self.mode != "RGB" and self.mode != "L":
- raise ValueError(
- "only RGB or L mode images can be quantized to a palette"
- )
+ msg = "only RGB or L mode images can be quantized to a palette"
+ raise ValueError(msg)
im = self.im.convert("P", dither, palette.im)
new_im = self._new(im)
new_im.palette = palette.palette.copy()
@@ -1173,9 +1223,11 @@ class Image:
return self.copy()
if box[2] < box[0]:
- raise ValueError("Coordinate 'right' is less than 'left'")
+ msg = "Coordinate 'right' is less than 'left'"
+ raise ValueError(msg)
elif box[3] < box[1]:
- raise ValueError("Coordinate 'lower' is less than 'upper'")
+ msg = "Coordinate 'lower' is less than 'upper'"
+ raise ValueError(msg)
self.load()
return self._new(self._crop(self.im, box))
@@ -1243,9 +1295,8 @@ class Image:
if isinstance(filter, Callable):
filter = filter()
if not hasattr(filter, "filter"):
- raise TypeError(
- "filter argument should be ImageFilter.Filter instance or class"
- )
+ msg = "filter argument should be ImageFilter.Filter instance or class"
+ raise TypeError(msg)
multiband = isinstance(filter, ImageFilter.MultibandFilter)
if self.im.bands == 1 or multiband:
@@ -1416,6 +1467,49 @@ class Image:
self._exif._loaded = False
self.getexif()
+ def get_child_images(self):
+ child_images = []
+ exif = self.getexif()
+ ifds = []
+ if ExifTags.Base.SubIFDs in exif:
+ subifd_offsets = exif[ExifTags.Base.SubIFDs]
+ if subifd_offsets:
+ if not isinstance(subifd_offsets, tuple):
+ subifd_offsets = (subifd_offsets,)
+ for subifd_offset in subifd_offsets:
+ ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset))
+ ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
+ if ifd1 and ifd1.get(513):
+ ifds.append((ifd1, exif._info.next))
+
+ offset = None
+ for ifd, ifd_offset in ifds:
+ current_offset = self.fp.tell()
+ if offset is None:
+ offset = current_offset
+
+ fp = self.fp
+ thumbnail_offset = ifd.get(513)
+ if thumbnail_offset is not None:
+ try:
+ thumbnail_offset += self._exif_offset
+ except AttributeError:
+ pass
+ self.fp.seek(thumbnail_offset)
+ data = self.fp.read(ifd.get(514))
+ fp = io.BytesIO(data)
+
+ with open(fp) as im:
+ if thumbnail_offset is None:
+ im._frame_pos = [ifd_offset]
+ im._seek(0)
+ im.load()
+ child_images.append(im)
+
+ if offset is not None:
+ self.fp.seek(offset)
+ return child_images
+
def getim(self):
"""
Returns a capsule that points to the internal image memory.
@@ -1451,7 +1545,8 @@ class Image:
def apply_transparency(self):
"""
If a P mode image has a "transparency" key in the info dictionary,
- remove the key and apply the transparency to the palette instead.
+ remove the key and instead apply the transparency to the palette.
+ Otherwise, the image is unchanged.
"""
if self.mode != "P" or "transparency" not in self.info:
return
@@ -1610,7 +1705,8 @@ class Image:
size = mask.size
else:
# FIXME: use self.size here?
- raise ValueError("cannot determine region size; use 4-item box")
+ msg = "cannot determine region size; use 4-item box"
+ raise ValueError(msg)
box += (box[0] + size[0], box[1] + size[1])
if isinstance(im, str):
@@ -1649,15 +1745,20 @@ class Image:
"""
if not isinstance(source, (list, tuple)):
- raise ValueError("Source must be a tuple")
+ msg = "Source must be a tuple"
+ raise ValueError(msg)
if not isinstance(dest, (list, tuple)):
- raise ValueError("Destination must be a tuple")
+ msg = "Destination must be a tuple"
+ raise ValueError(msg)
if not len(source) in (2, 4):
- raise ValueError("Source must be a 2 or 4-tuple")
+ msg = "Source must be a 2 or 4-tuple"
+ raise ValueError(msg)
if not len(dest) == 2:
- raise ValueError("Destination must be a 2-tuple")
+ msg = "Destination must be a 2-tuple"
+ raise ValueError(msg)
if min(source) < 0:
- raise ValueError("Source must be non-negative")
+ msg = "Source must be non-negative"
+ raise ValueError(msg)
if len(source) == 2:
source = source + im.size
@@ -1722,7 +1823,8 @@ class Image:
if self.mode == "F":
# FIXME: _imaging returns a confusing error message for this case
- raise ValueError("point operation not supported for this mode")
+ msg = "point operation not supported for this mode"
+ raise ValueError(msg)
if mode != "F":
lut = [round(i) for i in lut]
@@ -1756,7 +1858,8 @@ class Image:
self.pyaccess = None
self.mode = self.im.mode
except KeyError as e:
- raise ValueError("illegal image mode") from e
+ msg = "illegal image mode"
+ raise ValueError(msg) from e
if self.mode in ("LA", "PA"):
band = 1
@@ -1766,7 +1869,8 @@ class Image:
if isImageType(alpha):
# alpha layer
if alpha.mode not in ("1", "L"):
- raise ValueError("illegal image mode")
+ msg = "illegal image mode"
+ raise ValueError(msg)
alpha.load()
if alpha.mode == "1":
alpha = alpha.convert("L")
@@ -1822,7 +1926,8 @@ class Image:
from . import ImagePalette
if self.mode not in ("L", "LA", "P", "PA"):
- raise ValueError("illegal image mode")
+ msg = "illegal image mode"
+ raise ValueError(msg)
if isinstance(data, ImagePalette.ImagePalette):
palette = ImagePalette.raw(data.rawmode, data.palette)
else:
@@ -1891,7 +1996,8 @@ class Image:
from . import ImagePalette
if self.mode not in ("L", "P"):
- raise ValueError("illegal image mode")
+ msg = "illegal image mode"
+ raise ValueError(msg)
bands = 3
palette_mode = "RGB"
@@ -2023,7 +2129,7 @@ class Image:
Resampling.BOX,
Resampling.HAMMING,
):
- message = f"Unknown resampling filter ({resample})."
+ msg = f"Unknown resampling filter ({resample})."
filters = [
f"{filter[1]} ({filter[0]})"
@@ -2036,12 +2142,12 @@ class Image:
(Resampling.HAMMING, "Image.Resampling.HAMMING"),
)
]
- raise ValueError(
- message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
- )
+ msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
+ raise ValueError(msg)
if reducing_gap is not None and reducing_gap < 1.0:
- raise ValueError("reducing_gap must be 1.0 or greater")
+ msg = "reducing_gap must be 1.0 or greater"
+ raise ValueError(msg)
size = tuple(size)
@@ -2299,7 +2405,8 @@ class Image:
try:
format = EXTENSION[ext]
except KeyError as e:
- raise ValueError(f"unknown file extension: {ext}") from e
+ msg = f"unknown file extension: {ext}"
+ raise ValueError(msg) from e
if format.upper() not in SAVE:
init()
@@ -2413,7 +2520,8 @@ class Image:
try:
channel = self.getbands().index(channel)
except ValueError as e:
- raise ValueError(f'The image has no channel "{channel}"') from e
+ msg = f'The image has no channel "{channel}"'
+ raise ValueError(msg) from e
return self._new(self.im.getband(channel))
@@ -2584,7 +2692,8 @@ class Image:
method, data = method.getdata()
if data is None:
- raise ValueError("missing method data")
+ msg = "missing method data"
+ raise ValueError(msg)
im = new(self.mode, size, fillcolor)
if self.mode == "P" and self.palette:
@@ -2645,7 +2754,8 @@ class Image:
)
else:
- raise ValueError("unknown transformation method")
+ msg = "unknown transformation method"
+ raise ValueError(msg)
if resample not in (
Resampling.NEAREST,
@@ -2653,13 +2763,13 @@ class Image:
Resampling.BICUBIC,
):
if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS):
- message = {
+ msg = {
Resampling.BOX: "Image.Resampling.BOX",
Resampling.HAMMING: "Image.Resampling.HAMMING",
Resampling.LANCZOS: "Image.Resampling.LANCZOS",
}[resample] + f" ({resample}) cannot be used."
else:
- message = f"Unknown resampling filter ({resample})."
+ msg = f"Unknown resampling filter ({resample})."
filters = [
f"{filter[1]} ({filter[0]})"
@@ -2669,9 +2779,8 @@ class Image:
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
)
]
- raise ValueError(
- message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
- )
+ msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1]
+ raise ValueError(msg)
image.load()
@@ -2710,7 +2819,8 @@ class Image:
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.toqimage(self)
def toqpixmap(self):
@@ -2718,7 +2828,8 @@ class Image:
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.toqpixmap(self)
@@ -2766,11 +2877,14 @@ def _check_size(size):
"""
if not isinstance(size, (list, tuple)):
- raise ValueError("Size must be a tuple")
+ msg = "Size must be a tuple"
+ raise ValueError(msg)
if len(size) != 2:
- raise ValueError("Size must be a tuple of length 2")
+ msg = "Size must be a tuple of length 2"
+ raise ValueError(msg)
if size[0] < 0 or size[1] < 0:
- raise ValueError("Width and height must be >= 0")
+ msg = "Width and height must be >= 0"
+ raise ValueError(msg)
return True
@@ -2956,11 +3070,13 @@ def fromarray(obj, mode=None):
try:
typekey = (1, 1) + shape[2:], arr["typestr"]
except KeyError as e:
- raise TypeError("Cannot handle this data type") from e
+ msg = "Cannot handle this data type"
+ raise TypeError(msg) from e
try:
mode, rawmode = _fromarray_typemap[typekey]
except KeyError as e:
- raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e
+ msg = "Cannot handle this data type: %s, %s" % typekey
+ raise TypeError(msg) from e
else:
rawmode = mode
if mode in ["1", "L", "I", "P", "F"]:
@@ -2970,7 +3086,8 @@ def fromarray(obj, mode=None):
else:
ndmax = 4
if ndim > ndmax:
- raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.")
+ msg = f"Too many dimensions: {ndim} > {ndmax}."
+ raise ValueError(msg)
size = 1 if ndim == 1 else shape[1], shape[0]
if strides is not None:
@@ -2987,7 +3104,8 @@ def fromqimage(im):
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.fromqimage(im)
@@ -2996,7 +3114,8 @@ def fromqpixmap(im):
from . import ImageQt
if not ImageQt.qt_is_installed:
- raise ImportError("Qt bindings are not installed")
+ msg = "Qt bindings are not installed"
+ raise ImportError(msg)
return ImageQt.fromqpixmap(im)
@@ -3034,10 +3153,11 @@ def _decompression_bomb_check(size):
pixels = size[0] * size[1]
if pixels > 2 * MAX_IMAGE_PIXELS:
- raise DecompressionBombError(
+ msg = (
f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} "
"pixels, could be decompression bomb DOS attack."
)
+ raise DecompressionBombError(msg)
if pixels > MAX_IMAGE_PIXELS:
warnings.warn(
@@ -3077,17 +3197,20 @@ def open(fp, mode="r", formats=None):
"""
if mode != "r":
- raise ValueError(f"bad mode {repr(mode)}")
+ msg = f"bad mode {repr(mode)}"
+ raise ValueError(msg)
elif isinstance(fp, io.StringIO):
- raise ValueError(
+ msg = (
"StringIO cannot be used to open an image. "
"Binary data must be used instead."
)
+ raise ValueError(msg)
if formats is None:
formats = ID
elif not isinstance(formats, (list, tuple)):
- raise TypeError("formats must be a list or tuple")
+ msg = "formats must be a list or tuple"
+ raise TypeError(msg)
exclusive_fp = False
filename = ""
@@ -3152,9 +3275,8 @@ def open(fp, mode="r", formats=None):
fp.close()
for message in accept_warnings:
warnings.warn(message)
- raise UnidentifiedImageError(
- "cannot identify image file %r" % (filename if filename else fp)
- )
+ msg = "cannot identify image file %r" % (filename if filename else fp)
+ raise UnidentifiedImageError(msg)
#
@@ -3245,12 +3367,15 @@ def merge(mode, bands):
"""
if getmodebands(mode) != len(bands) or "*" in mode:
- raise ValueError("wrong number of bands")
+ msg = "wrong number of bands"
+ raise ValueError(msg)
for band in bands[1:]:
if band.mode != getmodetype(mode):
- raise ValueError("mode mismatch")
+ msg = "mode mismatch"
+ raise ValueError(msg)
if band.size != bands[0].size:
- raise ValueError("size mismatch")
+ msg = "size mismatch"
+ raise ValueError(msg)
for band in bands:
band.load()
return bands[0]._new(core.merge(mode, *[b.im for b in bands]))
@@ -3337,8 +3462,7 @@ def registered_extensions():
Returns a dictionary containing all file extensions belonging
to registered plugins
"""
- if not EXTENSION:
- init()
+ init()
return EXTENSION
@@ -3472,6 +3596,7 @@ class Exif(MutableMapping):
def __init__(self):
self._data = {}
+ self._hidden_data = {}
self._ifds = {}
self._info = None
self._loaded_exif = None
@@ -3525,6 +3650,7 @@ class Exif(MutableMapping):
return
self._loaded_exif = data
self._data.clear()
+ self._hidden_data.clear()
self._ifds.clear()
if data and data.startswith(b"Exif\x00\x00"):
data = data[6:]
@@ -3545,6 +3671,7 @@ class Exif(MutableMapping):
def load_from_fp(self, fp, offset=None):
self._loaded_exif = None
self._data.clear()
+ self._hidden_data.clear()
self._ifds.clear()
# process dictionary
@@ -3567,14 +3694,16 @@ class Exif(MutableMapping):
merged_dict = dict(self)
# get EXIF extension
- if 0x8769 in self:
- ifd = self._get_ifd_dict(self[0x8769])
+ if ExifTags.IFD.Exif in self:
+ ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif])
if ifd:
merged_dict.update(ifd)
# GPS
- if 0x8825 in self:
- merged_dict[0x8825] = self._get_ifd_dict(self[0x8825])
+ if ExifTags.IFD.GPSInfo in self:
+ merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict(
+ self[ExifTags.IFD.GPSInfo]
+ )
return merged_dict
@@ -3584,31 +3713,35 @@ class Exif(MutableMapping):
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):
+ if tag in [
+ ExifTags.IFD.Exif,
+ ExifTags.IFD.GPSInfo,
+ ] and not isinstance(value, dict):
value = self.get_ifd(tag)
if (
- tag == 0x8769
- and 0xA005 in value
- and not isinstance(value[0xA005], dict)
+ tag == ExifTags.IFD.Exif
+ and ExifTags.IFD.Interop in value
+ and not isinstance(value[ExifTags.IFD.Interop], dict)
):
value = value.copy()
- value[0xA005] = self.get_ifd(0xA005)
+ value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop)
ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag):
if tag not in self._ifds:
- if tag in [0x8769, 0x8825]:
- # exif, gpsinfo
- if tag in self:
- self._ifds[tag] = self._get_ifd_dict(self[tag])
- elif tag in [0xA005, 0x927C]:
- # interop, makernote
- if 0x8769 not in self._ifds:
- self.get_ifd(0x8769)
- tag_data = self._ifds[0x8769][tag]
- if tag == 0x927C:
- # makernote
+ if tag == ExifTags.IFD.IFD1:
+ if self._info is not None and self._info.next != 0:
+ self._ifds[tag] = self._get_ifd_dict(self._info.next)
+ elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
+ offset = self._hidden_data.get(tag, self.get(tag))
+ if offset is not None:
+ self._ifds[tag] = self._get_ifd_dict(offset)
+ elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
+ if ExifTags.IFD.Exif not in self._ifds:
+ self.get_ifd(ExifTags.IFD.Exif)
+ tag_data = self._ifds[ExifTags.IFD.Exif][tag]
+ if tag == ExifTags.IFD.Makernote:
from .TiffImagePlugin import ImageFileDirectory_v2
if tag_data[:8] == b"FUJIFILM":
@@ -3684,9 +3817,22 @@ class Exif(MutableMapping):
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
self._ifds[tag] = makernote
else:
- # interop
+ # Interop
self._ifds[tag] = self._get_ifd_dict(tag_data)
- return self._ifds.get(tag, {})
+ ifd = self._ifds.get(tag, {})
+ if tag == ExifTags.IFD.Exif and self._hidden_data:
+ ifd = {
+ k: v
+ for (k, v) in ifd.items()
+ if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote)
+ }
+ return ifd
+
+ def hide_offsets(self):
+ for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo):
+ if tag in self:
+ self._hidden_data[tag] = self[tag]
+ del self[tag]
def __str__(self):
if self._info is not None:
diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py
index 605252d5d..f87849680 100644
--- a/src/PIL/ImageCms.py
+++ b/src/PIL/ImageCms.py
@@ -124,7 +124,8 @@ def __getattr__(name):
if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
#
@@ -191,7 +192,8 @@ class ImageCmsProfile:
elif isinstance(profile, _imagingcms.CmsProfile):
self._set(profile)
else:
- raise TypeError("Invalid type for Profile")
+ msg = "Invalid type for Profile"
+ raise TypeError(msg)
def _set(self, profile, filename=None):
self.profile = profile
@@ -269,7 +271,8 @@ class ImageCmsTransform(Image.ImagePointHandler):
def apply_in_place(self, im):
im.load()
if im.mode != self.output_mode:
- raise ValueError("mode mismatch") # wrong output mode
+ msg = "mode mismatch"
+ raise ValueError(msg) # wrong output mode
self.transform.apply(im.im.id, im.im.id)
im.info["icc_profile"] = self.output_profile.tobytes()
return im
@@ -374,10 +377,12 @@ def profileToProfile(
outputMode = im.mode
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
- raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}")
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
+ raise PyCMSError(msg)
try:
if not isinstance(inputProfile, ImageCmsProfile):
@@ -489,10 +494,12 @@ def buildTransform(
"""
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
- raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+ msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ raise PyCMSError(msg)
try:
if not isinstance(inputProfile, ImageCmsProfile):
@@ -591,10 +598,12 @@ def buildProofTransform(
"""
if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
- raise PyCMSError("renderingIntent must be an integer between 0 and 3")
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
- raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG)
+ msg = "flags must be an integer between 0 and %s" + _MAX_FLAG
+ raise PyCMSError(msg)
try:
if not isinstance(inputProfile, ImageCmsProfile):
@@ -705,17 +714,17 @@ def createProfile(colorSpace, colorTemp=-1):
"""
if colorSpace not in ["LAB", "XYZ", "sRGB"]:
- raise PyCMSError(
+ msg = (
f"Color space not supported for on-the-fly profile creation ({colorSpace})"
)
+ raise PyCMSError(msg)
if colorSpace == "LAB":
try:
colorTemp = float(colorTemp)
except (TypeError, ValueError) as e:
- raise PyCMSError(
- f'Color temperature must be numeric, "{colorTemp}" not valid'
- ) from e
+ msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
+ raise PyCMSError(msg) from e
try:
return core.createProfile(colorSpace, colorTemp)
diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py
index 9cbce4143..e184ed68d 100644
--- a/src/PIL/ImageColor.py
+++ b/src/PIL/ImageColor.py
@@ -33,7 +33,8 @@ def getrgb(color):
:return: ``(red, green, blue[, alpha])``
"""
if len(color) > 100:
- raise ValueError("color specifier is too long")
+ msg = "color specifier is too long"
+ raise ValueError(msg)
color = color.lower()
rgb = colormap.get(color, None)
@@ -115,7 +116,8 @@ def getrgb(color):
m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
if m:
return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
- raise ValueError(f"unknown color specifier: {repr(color)}")
+ msg = f"unknown color specifier: {repr(color)}"
+ raise ValueError(msg)
def getcolor(color, mode):
diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py
index e84dafb12..ce29a163b 100644
--- a/src/PIL/ImageDraw.py
+++ b/src/PIL/ImageDraw.py
@@ -69,7 +69,8 @@ class ImageDraw:
if mode == "RGBA" and im.mode == "RGB":
blend = 1
else:
- raise ValueError("mode mismatch")
+ msg = "mode mismatch"
+ raise ValueError(msg)
if mode == "P":
self.palette = im.palette
else:
@@ -87,17 +88,25 @@ class ImageDraw:
self.fontmode = "1"
else:
self.fontmode = "L" # aliasing is okay for other modes
- self.fill = 0
+ self.fill = False
def getfont(self):
"""
Get the current default font.
+ To set the default font for this ImageDraw instance::
+
+ from PIL import ImageDraw, ImageFont
+ draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
To set the default font for all future ImageDraw instances::
from PIL import ImageDraw, ImageFont
ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+ If the current default font is ``None``,
+ it is initialized with ``ImageFont.load_default()``.
+
:returns: An image font."""
if not self.font:
# FIXME: should add a font repository
@@ -429,7 +438,8 @@ class ImageDraw:
)
if embedded_color and self.mode not in ("RGB", "RGBA"):
- raise ValueError("Embedded color supported only in RGB and RGBA modes")
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
if font is None:
font = self.getfont()
@@ -444,7 +454,11 @@ class ImageDraw:
mode = self.fontmode
if stroke_width == 0 and embedded_color:
mode = "RGBA"
- coord = xy
+ coord = []
+ start = []
+ for i in range(2):
+ coord.append(int(xy[i]))
+ start.append(math.modf(xy[i])[0])
try:
mask, offset = font.getmask2(
text,
@@ -455,6 +469,7 @@ class ImageDraw:
stroke_width=stroke_width,
anchor=anchor,
ink=ink,
+ start=start,
*args,
**kwargs,
)
@@ -470,6 +485,7 @@ class ImageDraw:
stroke_width,
anchor,
ink,
+ start=start,
*args,
**kwargs,
)
@@ -482,8 +498,8 @@ class ImageDraw:
# extract mask and set text alpha
color, mask = mask, mask.getband(3)
color.fillband(3, (ink >> 24) & 0xFF)
- coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1]
- self.im.paste(color, coord + coord2, mask)
+ x, y = coord
+ self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else:
self.draw.draw_bitmap(coord, mask, ink)
@@ -520,14 +536,17 @@ class ImageDraw:
embedded_color=False,
):
if direction == "ttb":
- raise ValueError("ttb direction is unsupported for multiline text")
+ msg = "ttb direction is unsupported for multiline text"
+ raise ValueError(msg)
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
- raise ValueError("anchor must be a 2 character string")
+ msg = "anchor must be a 2 character string"
+ raise ValueError(msg)
elif anchor[1] in "tb":
- raise ValueError("anchor not supported for multiline text")
+ msg = "anchor not supported for multiline text"
+ raise ValueError(msg)
widths = []
max_width = 0
@@ -564,7 +583,8 @@ class ImageDraw:
elif align == "right":
left += width_difference
else:
- raise ValueError('align must be "left", "center" or "right"')
+ msg = 'align must be "left", "center" or "right"'
+ raise ValueError(msg)
self.text(
(left, top),
@@ -658,9 +678,11 @@ class ImageDraw:
):
"""Get the length of a given string, in pixels with 1/64 precision."""
if self._multiline_check(text):
- raise ValueError("can't measure length of multiline text")
+ msg = "can't measure length of multiline text"
+ raise ValueError(msg)
if embedded_color and self.mode not in ("RGB", "RGBA"):
- raise ValueError("Embedded color supported only in RGB and RGBA modes")
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
if font is None:
font = self.getfont()
@@ -698,7 +720,8 @@ class ImageDraw:
):
"""Get the bounding box of a given string, in pixels."""
if embedded_color and self.mode not in ("RGB", "RGBA"):
- raise ValueError("Embedded color supported only in RGB and RGBA modes")
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
if self._multiline_check(text):
return self.multiline_textbbox(
@@ -738,14 +761,17 @@ class ImageDraw:
embedded_color=False,
):
if direction == "ttb":
- raise ValueError("ttb direction is unsupported for multiline text")
+ msg = "ttb direction is unsupported for multiline text"
+ raise ValueError(msg)
if anchor is None:
anchor = "la"
elif len(anchor) != 2:
- raise ValueError("anchor must be a 2 character string")
+ msg = "anchor must be a 2 character string"
+ raise ValueError(msg)
elif anchor[1] in "tb":
- raise ValueError("anchor not supported for multiline text")
+ msg = "anchor not supported for multiline text"
+ raise ValueError(msg)
widths = []
max_width = 0
@@ -789,7 +815,8 @@ class ImageDraw:
elif align == "right":
left += width_difference
else:
- raise ValueError('align must be "left", "center" or "right"')
+ msg = 'align must be "left", "center" or "right"'
+ raise ValueError(msg)
bbox_line = self.textbbox(
(left, top),
@@ -965,38 +992,44 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
# 1. Error Handling
# 1.1 Check `n_sides` has an appropriate value
if not isinstance(n_sides, int):
- raise TypeError("n_sides should be an int")
+ msg = "n_sides should be an int"
+ raise TypeError(msg)
if n_sides < 3:
- raise ValueError("n_sides should be an int > 2")
+ msg = "n_sides should be an int > 2"
+ raise ValueError(msg)
# 1.2 Check `bounding_circle` has an appropriate value
if not isinstance(bounding_circle, (list, tuple)):
- raise TypeError("bounding_circle should be a tuple")
+ msg = "bounding_circle should be a tuple"
+ raise TypeError(msg)
if len(bounding_circle) == 3:
*centroid, polygon_radius = bounding_circle
elif len(bounding_circle) == 2:
centroid, polygon_radius = bounding_circle
else:
- raise ValueError(
+ msg = (
"bounding_circle should contain 2D coordinates "
"and a radius (e.g. (x, y, r) or ((x, y), r) )"
)
+ raise ValueError(msg)
if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)):
- raise ValueError("bounding_circle should only contain numeric data")
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
if not len(centroid) == 2:
- raise ValueError(
- "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
- )
+ msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
+ raise ValueError(msg)
if polygon_radius <= 0:
- raise ValueError("bounding_circle radius should be > 0")
+ msg = "bounding_circle radius should be > 0"
+ raise ValueError(msg)
# 1.3 Check `rotation` has an appropriate value
if not isinstance(rotation, (int, float)):
- raise ValueError("rotation should be an int or float")
+ msg = "rotation should be an int or float"
+ raise ValueError(msg)
# 2. Define Helper Functions
def _apply_rotation(point, degrees, centroid):
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index f281b9e14..12391955f 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -63,12 +63,13 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
def raise_oserror(error):
try:
- message = Image.core.getcodecstatus(error)
+ msg = Image.core.getcodecstatus(error)
except AttributeError:
- message = ERRORS.get(error)
- if not message:
- message = f"decoder error {error}"
- raise OSError(message + " when reading image file")
+ msg = ERRORS.get(error)
+ if not msg:
+ msg = f"decoder error {error}"
+ msg += " when reading image file"
+ raise OSError(msg)
def _tilesort(t):
@@ -124,7 +125,8 @@ class ImageFile(Image.Image):
raise SyntaxError(v) from v
if not self.mode or self.size[0] <= 0 or self.size[1] <= 0:
- raise SyntaxError("not identified by this driver")
+ msg = "not identified by this driver"
+ raise SyntaxError(msg)
except BaseException:
# close the file only if we have opened it this constructor
if self._exclusive_fp:
@@ -137,6 +139,10 @@ class ImageFile(Image.Image):
if self.format is not None:
return Image.MIME.get(self.format.upper())
+ def __setstate__(self, state):
+ self.tile = []
+ super().__setstate__(state)
+
def verify(self):
"""Check file integrity"""
@@ -150,7 +156,8 @@ class ImageFile(Image.Image):
"""Load image data based on tile list"""
if self.tile is None:
- raise OSError("cannot load this image")
+ msg = "cannot load this image"
+ raise OSError(msg)
pixel = Image.Image.load(self)
if not self.tile:
@@ -245,16 +252,18 @@ class ImageFile(Image.Image):
if LOAD_TRUNCATED_IMAGES:
break
else:
- raise OSError("image file is truncated") from e
+ msg = "image file is truncated"
+ raise OSError(msg) from e
if not s: # truncated jpeg
if LOAD_TRUNCATED_IMAGES:
break
else:
- raise OSError(
+ msg = (
"image file is truncated "
f"({len(b)} bytes not processed)"
)
+ raise OSError(msg)
b = b + s
n, err_code = decoder.decode(b)
@@ -310,7 +319,8 @@ class ImageFile(Image.Image):
and frame >= self.n_frames + self._min_frame
)
):
- raise EOFError("attempt to seek outside sequence")
+ msg = "attempt to seek outside sequence"
+ raise EOFError(msg)
return self.tell() != frame
@@ -324,12 +334,14 @@ class StubImageFile(ImageFile):
"""
def _open(self):
- raise NotImplementedError("StubImageFile subclass must implement _open")
+ msg = "StubImageFile subclass must implement _open"
+ raise NotImplementedError(msg)
def load(self):
loader = self._load()
if loader is None:
- raise OSError(f"cannot find loader for this {self.format} file")
+ msg = f"cannot find loader for this {self.format} file"
+ raise OSError(msg)
image = loader.load(self)
assert image is not None
# become the other object (!)
@@ -339,7 +351,8 @@ class StubImageFile(ImageFile):
def _load(self):
"""(Hook) Find actual image loader."""
- raise NotImplementedError("StubImageFile subclass must implement _load")
+ msg = "StubImageFile subclass must implement _load"
+ raise NotImplementedError(msg)
class Parser:
@@ -464,9 +477,11 @@ class Parser:
self.feed(b"")
self.data = self.decoder = None
if not self.finished:
- raise OSError("image was incomplete")
+ msg = "image was incomplete"
+ raise OSError(msg)
if not self.image:
- raise OSError("cannot parse this image")
+ msg = "cannot parse this image"
+ raise OSError(msg)
if self.data:
# incremental parsing not possible; reopen the file
# not that we have all data
@@ -531,7 +546,8 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
# slight speedup: compress to real file object
s = encoder.encode_to_file(fh, bufsize)
if s < 0:
- raise OSError(f"encoder error {s} when writing image file") from exc
+ msg = f"encoder error {s} when writing image file"
+ raise OSError(msg) from exc
finally:
encoder.cleanup()
@@ -554,7 +570,8 @@ def _safe_read(fp, size):
if size <= SAFEBLOCK:
data = fp.read(size)
if len(data) < size:
- raise OSError("Truncated File Read")
+ msg = "Truncated File Read"
+ raise OSError(msg)
return data
data = []
remaining_size = size
@@ -565,7 +582,8 @@ def _safe_read(fp, size):
data.append(block)
remaining_size -= len(block)
if sum(len(d) for d in data) < size:
- raise OSError("Truncated File Read")
+ msg = "Truncated File Read"
+ raise OSError(msg)
return b"".join(data)
@@ -641,13 +659,15 @@ class PyCodec:
self.state.ysize = y1 - y0
if self.state.xsize <= 0 or self.state.ysize <= 0:
- raise ValueError("Size cannot be negative")
+ msg = "Size cannot be negative"
+ raise ValueError(msg)
if (
self.state.xsize + self.state.xoff > self.im.size[0]
or self.state.ysize + self.state.yoff > self.im.size[1]
):
- raise ValueError("Tile cannot extend outside image")
+ msg = "Tile cannot extend outside image"
+ raise ValueError(msg)
class PyDecoder(PyCodec):
@@ -692,9 +712,11 @@ class PyDecoder(PyCodec):
s = d.decode(data)
if s[0] >= 0:
- raise ValueError("not enough image data")
+ msg = "not enough image data"
+ raise ValueError(msg)
if s[1] != 0:
- raise ValueError("cannot decode image data")
+ msg = "cannot decode image data"
+ raise ValueError(msg)
class PyEncoder(PyCodec):
diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py
index e10c6fdf1..59e2c18b9 100644
--- a/src/PIL/ImageFilter.py
+++ b/src/PIL/ImageFilter.py
@@ -28,7 +28,8 @@ class MultibandFilter(Filter):
class BuiltinFilter(MultibandFilter):
def filter(self, image):
if image.mode == "P":
- raise ValueError("cannot filter palette images")
+ msg = "cannot filter palette images"
+ raise ValueError(msg)
return image.filter(*self.filterargs)
@@ -57,7 +58,8 @@ class Kernel(BuiltinFilter):
# default scale is sum of kernel
scale = functools.reduce(lambda a, b: a + b, kernel)
if size[0] * size[1] != len(kernel):
- raise ValueError("not enough coefficients in kernel")
+ msg = "not enough coefficients in kernel"
+ raise ValueError(msg)
self.filterargs = size, scale, offset, kernel
@@ -80,7 +82,8 @@ class RankFilter(Filter):
def filter(self, image):
if image.mode == "P":
- raise ValueError("cannot filter palette images")
+ msg = "cannot filter palette images"
+ raise ValueError(msg)
image = image.expand(self.size // 2, self.size // 2)
return image.rankfilter(self.size, self.rank)
@@ -355,7 +358,8 @@ class Color3DLUT(MultibandFilter):
def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
if channels not in (3, 4):
- raise ValueError("Only 3 or 4 output channels are supported")
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
self.size = size = self._check_size(size)
self.channels = channels
self.mode = target_mode
@@ -395,19 +399,21 @@ class Color3DLUT(MultibandFilter):
table, raw_table = [], table
for pixel in raw_table:
if len(pixel) != channels:
- raise ValueError(
+ msg = (
"The elements of the table should "
- "have a length of {}.".format(channels)
+ f"have a length of {channels}."
)
+ raise ValueError(msg)
table.extend(pixel)
if wrong_size or len(table) != items * channels:
- raise ValueError(
+ msg = (
"The table should have either channels * size**3 float items "
"or size**3 items of channels-sized tuples with floats. "
f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. "
f"Actual length: {len(table)}"
)
+ raise ValueError(msg)
self.table = table
@staticmethod
@@ -415,15 +421,15 @@ class Color3DLUT(MultibandFilter):
try:
_, _, _ = size
except ValueError as e:
- raise ValueError(
- "Size should be either an integer or a tuple of three integers."
- ) from e
+ msg = "Size should be either an integer or a tuple of three integers."
+ raise ValueError(msg) from e
except TypeError:
size = (size, size, size)
size = [int(x) for x in size]
for size_1d in size:
if not 2 <= size_1d <= 65:
- raise ValueError("Size should be in [2, 65] range.")
+ msg = "Size should be in [2, 65] range."
+ raise ValueError(msg)
return size
@classmethod
@@ -441,7 +447,8 @@ class Color3DLUT(MultibandFilter):
"""
size_1d, size_2d, size_3d = cls._check_size(size)
if channels not in (3, 4):
- raise ValueError("Only 3 or 4 output channels are supported")
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
table = [0] * (size_1d * size_2d * size_3d * channels)
idx_out = 0
@@ -481,7 +488,8 @@ class Color3DLUT(MultibandFilter):
lookup table.
"""
if channels not in (None, 3, 4):
- raise ValueError("Only 3 or 4 output channels are supported")
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
ch_in = self.channels
ch_out = channels or ch_in
size_1d, size_2d, size_3d = self.size
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
index 8be7f0f10..b144c3dd2 100644
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -26,6 +26,7 @@
#
import base64
+import math
import os
import sys
import warnings
@@ -49,13 +50,15 @@ def __getattr__(name):
if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
class _ImagingFtNotInstalled:
# module placeholder
def __getattr__(self, id):
- raise ImportError("The _imagingft C module is not installed")
+ msg = "The _imagingft C module is not installed"
+ raise ImportError(msg)
try:
@@ -104,7 +107,8 @@ class ImageFont:
else:
if image:
image.close()
- raise OSError("cannot find glyph data file")
+ msg = "cannot find glyph data file"
+ raise OSError(msg)
self.file = fullname
@@ -115,7 +119,8 @@ class ImageFont:
# read PILfont header
if file.readline() != b"PILfont\n":
- raise SyntaxError("Not a PILfont file")
+ msg = "Not a PILfont file"
+ raise SyntaxError(msg)
file.readline().split(b";")
self.info = [] # FIXME: should be a dictionary
while True:
@@ -129,7 +134,8 @@ class ImageFont:
# check image
if image.mode not in ("1", "L"):
- raise TypeError("invalid font image mode")
+ msg = "invalid font image mode"
+ raise TypeError(msg)
image.load()
@@ -588,6 +594,7 @@ class FreeTypeFont:
stroke_width=0,
anchor=None,
ink=0,
+ start=None,
):
"""
Create a bitmap for the text.
@@ -647,6 +654,11 @@ class FreeTypeFont:
.. versionadded:: 8.0.0
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.
"""
@@ -659,6 +671,7 @@ class FreeTypeFont:
stroke_width=stroke_width,
anchor=anchor,
ink=ink,
+ start=start,
)[0]
def getmask2(
@@ -672,6 +685,7 @@ class FreeTypeFont:
stroke_width=0,
anchor=None,
ink=0,
+ start=None,
*args,
**kwargs,
):
@@ -739,6 +753,11 @@ class FreeTypeFont:
.. versionadded:: 8.0.0
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
:return: A tuple of an internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking
@@ -750,12 +769,23 @@ class FreeTypeFont:
size, offset = self.font.getsize(
text, mode, direction, features, language, anchor
)
- size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
+ if start is None:
+ start = (0, 0)
+ size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(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
+ text,
+ im.id,
+ mode,
+ direction,
+ features,
+ language,
+ stroke_width,
+ ink,
+ start[0],
+ start[1],
)
return im, offset
@@ -792,7 +822,8 @@ class FreeTypeFont:
try:
names = self.font.getvarnames()
except AttributeError as e:
- raise NotImplementedError("FreeType 2.9.1 or greater is required") from e
+ msg = "FreeType 2.9.1 or greater is required"
+ raise NotImplementedError(msg) from e
return [name.replace(b"\x00", b"") for name in names]
def set_variation_by_name(self, name):
@@ -803,7 +834,7 @@ class FreeTypeFont:
names = self.get_variation_names()
if not isinstance(name, bytes):
name = name.encode()
- index = names.index(name)
+ index = names.index(name) + 1
if index == getattr(self, "_last_variation_index", None):
# When the same name is set twice in a row,
@@ -822,7 +853,8 @@ class FreeTypeFont:
try:
axes = self.font.getvaraxes()
except AttributeError as e:
- raise NotImplementedError("FreeType 2.9.1 or greater is required") from e
+ msg = "FreeType 2.9.1 or greater is required"
+ raise NotImplementedError(msg) from e
for axis in axes:
axis["name"] = axis["name"].replace(b"\x00", b"")
return axes
@@ -835,7 +867,8 @@ class FreeTypeFont:
try:
self.font.setvaraxes(axes)
except AttributeError as e:
- raise NotImplementedError("FreeType 2.9.1 or greater is required") from e
+ msg = "FreeType 2.9.1 or greater is required"
+ raise NotImplementedError(msg) from e
class TransposedFont:
@@ -889,9 +922,8 @@ class TransposedFont:
def getlength(self, text, *args, **kwargs):
if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
- raise ValueError(
- "text length is undefined for text rotated by 90 or 270 degrees"
- )
+ msg = "text length is undefined for text rotated by 90 or 270 degrees"
+ raise ValueError(msg)
return self.font.getlength(text, *args, **kwargs)
@@ -955,6 +987,11 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
encoding of any text provided in subsequent operations.
:param layout_engine: Which layout engine to use, if available:
:data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`.
+ If it is available, Raqm layout will be used by default.
+ Otherwise, basic layout will be used.
+
+ Raqm layout is recommended for all non-English text. If Raqm layout
+ is not required, basic layout will have better performance.
You can check support for Raqm layout using
:py:func:`PIL.features.check_feature` with ``feature="raqm"``.
@@ -1031,7 +1068,8 @@ def load_path(filename):
return load(os.path.join(directory, filename))
except OSError:
pass
- raise OSError("cannot find font file")
+ msg = "cannot find font file"
+ raise OSError(msg)
def load_default():
diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py
index 38074cb1b..982f77f20 100644
--- a/src/PIL/ImageGrab.py
+++ b/src/PIL/ImageGrab.py
@@ -75,7 +75,8 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N
return im
# use xdisplay=None for default display on non-win32/macOS systems
if not Image.core.HAVE_XCB:
- raise OSError("Pillow was built without XCB support")
+ msg = "Pillow was built without XCB support"
+ raise OSError(msg)
size, data = Image.core.grabscreen_x11(xdisplay)
im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
if bbox:
@@ -132,4 +133,17 @@ def grabclipboard():
return BmpImagePlugin.DibImageFile(data)
return None
else:
- raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only")
+ if shutil.which("wl-paste"):
+ args = ["wl-paste"]
+ elif shutil.which("xclip"):
+ args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
+ else:
+ msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
+ raise NotImplementedError(msg)
+ fh, filepath = tempfile.mkstemp()
+ subprocess.call(args, stdout=fh)
+ os.close(fh)
+ im = Image.open(filepath)
+ im.load()
+ os.unlink(filepath)
+ return im
diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py
index 09d9898d7..ac7d36b69 100644
--- a/src/PIL/ImageMath.py
+++ b/src/PIL/ImageMath.py
@@ -39,7 +39,8 @@ class _Operand:
elif im1.im.mode in ("I", "F"):
return im1.im
else:
- raise ValueError(f"unsupported mode: {im1.im.mode}")
+ msg = f"unsupported mode: {im1.im.mode}"
+ raise ValueError(msg)
else:
# argument was a constant
if _isconstant(im1) and self.im.mode in ("1", "L", "I"):
@@ -56,7 +57,8 @@ class _Operand:
try:
op = getattr(_imagingmath, op + "_" + im1.mode)
except AttributeError as e:
- raise TypeError(f"bad operand type for '{op}'") from e
+ msg = f"bad operand type for '{op}'"
+ raise TypeError(msg) from e
_imagingmath.unop(op, out.im.id, im1.im.id)
else:
# binary operation
@@ -80,7 +82,8 @@ class _Operand:
try:
op = getattr(_imagingmath, op + "_" + im1.mode)
except AttributeError as e:
- raise TypeError(f"bad operand type for '{op}'") from e
+ msg = f"bad operand type for '{op}'"
+ raise TypeError(msg) from e
_imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id)
return _Operand(out)
@@ -249,7 +252,8 @@ def eval(expression, _dict={}, **kw):
for name in code.co_names:
if name not in args and name != "abs":
- raise ValueError(f"'{name}' not allowed")
+ msg = f"'{name}' not allowed"
+ raise ValueError(msg)
scan(compiled_code)
out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args)
diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py
index 1e22c36a8..6fccc315b 100644
--- a/src/PIL/ImageMorph.py
+++ b/src/PIL/ImageMorph.py
@@ -81,7 +81,8 @@ class LutBuilder:
],
}
if op_name not in known_patterns:
- raise Exception("Unknown pattern " + op_name + "!")
+ msg = "Unknown pattern " + op_name + "!"
+ raise Exception(msg)
self.patterns = known_patterns[op_name]
@@ -145,7 +146,8 @@ class LutBuilder:
for p in self.patterns:
m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
if not m:
- raise Exception('Syntax error in pattern "' + p + '"')
+ msg = 'Syntax error in pattern "' + p + '"'
+ raise Exception(msg)
options = m.group(1)
pattern = m.group(2)
result = int(m.group(3))
@@ -193,10 +195,12 @@ class MorphOp:
Returns a tuple of the number of changed pixels and the
morphed image"""
if self.lut is None:
- raise Exception("No operator loaded")
+ msg = "No operator loaded"
+ raise Exception(msg)
if image.mode != "L":
- raise ValueError("Image mode must be L")
+ msg = "Image mode must be L"
+ raise ValueError(msg)
outimage = Image.new(image.mode, image.size, None)
count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id)
return count, outimage
@@ -208,10 +212,12 @@ class MorphOp:
Returns a list of tuples of (x,y) coordinates
of all matching pixels. See :ref:`coordinate-system`."""
if self.lut is None:
- raise Exception("No operator loaded")
+ msg = "No operator loaded"
+ raise Exception(msg)
if image.mode != "L":
- raise ValueError("Image mode must be L")
+ msg = "Image mode must be L"
+ raise ValueError(msg)
return _imagingmorph.match(bytes(self.lut), image.im.id)
def get_on_pixels(self, image):
@@ -221,7 +227,8 @@ class MorphOp:
of all matching pixels. See :ref:`coordinate-system`."""
if image.mode != "L":
- raise ValueError("Image mode must be L")
+ msg = "Image mode must be L"
+ raise ValueError(msg)
return _imagingmorph.get_on_pixels(image.im.id)
def load_lut(self, filename):
@@ -231,12 +238,14 @@ class MorphOp:
if len(self.lut) != LUT_SIZE:
self.lut = None
- raise Exception("Wrong size operator file!")
+ msg = "Wrong size operator file!"
+ raise Exception(msg)
def save_lut(self, filename):
"""Save an operator to an mrl file"""
if self.lut is None:
- raise Exception("No operator loaded")
+ msg = "No operator loaded"
+ raise Exception(msg)
with open(filename, "wb") as f:
f.write(self.lut)
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
index 443c540b6..e2168ce62 100644
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -49,13 +49,15 @@ def _color(color, mode):
def _lut(image, lut):
if image.mode == "P":
# FIXME: apply to lookup table, not image data
- raise NotImplementedError("mode P support coming soon")
+ msg = "mode P support coming soon"
+ raise NotImplementedError(msg)
elif image.mode in ("L", "RGB"):
if image.mode == "RGB" and len(lut) == 256:
lut = lut + lut + lut
return image.point(lut)
else:
- raise OSError("not supported for this image mode")
+ msg = "not supported for this image mode"
+ raise OSError(msg)
#
@@ -332,7 +334,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC):
if factor == 1:
return image.copy()
elif factor <= 0:
- raise ValueError("the factor must be greater than 0")
+ msg = "the factor must be greater than 0"
+ raise ValueError(msg)
else:
size = (round(factor * image.width), round(factor * image.height))
return image.resize(size, resample)
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
index b73b2cd9d..fe0d32155 100644
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -42,7 +42,8 @@ class ImagePalette:
if size != 0:
deprecate("The size parameter", 10, None)
if size != len(self.palette):
- raise ValueError("wrong palette size")
+ msg = "wrong palette size"
+ raise ValueError(msg)
@property
def palette(self):
@@ -97,7 +98,8 @@ class ImagePalette:
.. warning:: This method is experimental.
"""
if self.rawmode:
- raise ValueError("palette contains raw palette data")
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
if isinstance(self.palette, bytes):
return self.palette
arr = array.array("B", self.palette)
@@ -112,10 +114,14 @@ class ImagePalette:
.. warning:: This method is experimental.
"""
if self.rawmode:
- raise ValueError("palette contains raw palette data")
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
if isinstance(color, tuple):
if self.mode == "RGB":
- if len(color) == 4 and color[3] == 255:
+ if len(color) == 4:
+ if color[3] != 255:
+ msg = "cannot add non-opaque RGBA color to RGB palette"
+ raise ValueError(msg)
color = color[:3]
elif self.mode == "RGBA":
if len(color) == 3:
@@ -143,7 +149,8 @@ class ImagePalette:
index = i
break
if index >= 256:
- raise ValueError("cannot allocate more than 256 colors") from e
+ msg = "cannot allocate more than 256 colors"
+ raise ValueError(msg) from e
self.colors[color] = index
if index * 3 < len(self.palette):
self._palette = (
@@ -156,7 +163,8 @@ class ImagePalette:
self.dirty = 1
return index
else:
- raise ValueError(f"unknown color specifier: {repr(color)}")
+ msg = f"unknown color specifier: {repr(color)}"
+ raise ValueError(msg)
def save(self, fp):
"""Save palette to text file.
@@ -164,7 +172,8 @@ class ImagePalette:
.. warning:: This method is experimental.
"""
if self.rawmode:
- raise ValueError("palette contains raw palette data")
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
if isinstance(fp, str):
fp = open(fp, "w")
fp.write("# Palette\n")
@@ -259,6 +268,7 @@ def load(filename):
# traceback.print_exc()
pass
else:
- raise OSError("cannot load palette")
+ msg = "cannot load palette"
+ raise OSError(msg)
return lut # data, rawmode
diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py
index a34678c78..ad607a97b 100644
--- a/src/PIL/ImageQt.py
+++ b/src/PIL/ImageQt.py
@@ -179,7 +179,8 @@ def _toqclass_helper(im):
else:
if exclusive_fp:
im.close()
- raise ValueError(f"unsupported image mode {repr(im.mode)}")
+ msg = f"unsupported image mode {repr(im.mode)}"
+ raise ValueError(msg)
size = im.size
__data = data or align8to32(im.tobytes(), size[0], im.mode)
diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py
index 9df910a43..c4bb6334a 100644
--- a/src/PIL/ImageSequence.py
+++ b/src/PIL/ImageSequence.py
@@ -30,7 +30,8 @@ class Iterator:
def __init__(self, im):
if not hasattr(im, "seek"):
- raise AttributeError("im must have seek method")
+ msg = "im must have seek method"
+ raise AttributeError(msg)
self.im = im
self.position = getattr(self.im, "_min_frame", 0)
diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py
index 9f9a551fb..29d900bef 100644
--- a/src/PIL/ImageShow.py
+++ b/src/PIL/ImageShow.py
@@ -124,8 +124,9 @@ class Viewer:
deprecate("The 'file' argument", 10, "'path'")
path = options.pop("file")
else:
- raise TypeError("Missing required argument: 'path'")
- os.system(self.get_command(path, **options))
+ msg = "Missing required argument: 'path'"
+ raise TypeError(msg)
+ os.system(self.get_command(path, **options)) # nosec
return 1
@@ -136,7 +137,7 @@ class WindowsViewer(Viewer):
"""The default viewer on Windows is the default system application for PNG files."""
format = "PNG"
- options = {"compress_level": 1}
+ options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options):
return (
@@ -154,7 +155,7 @@ class MacViewer(Viewer):
"""The default viewer on macOS using ``Preview.app``."""
format = "PNG"
- options = {"compress_level": 1}
+ options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options):
# on darwin open returns immediately resulting in the temp
@@ -176,7 +177,8 @@ class MacViewer(Viewer):
deprecate("The 'file' argument", 10, "'path'")
path = options.pop("file")
else:
- raise TypeError("Missing required argument: 'path'")
+ msg = "Missing required argument: 'path'"
+ raise TypeError(msg)
subprocess.call(["open", "-a", "Preview.app", path])
executable = sys.executable or shutil.which("python3")
if executable:
@@ -197,7 +199,7 @@ if sys.platform == "darwin":
class UnixViewer(Viewer):
format = "PNG"
- options = {"compress_level": 1}
+ options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options):
command = self.get_command_ex(file, **options)[0]
@@ -226,7 +228,8 @@ class XDGViewer(UnixViewer):
deprecate("The 'file' argument", 10, "'path'")
path = options.pop("file")
else:
- raise TypeError("Missing required argument: 'path'")
+ msg = "Missing required argument: 'path'"
+ raise TypeError(msg)
subprocess.Popen(["xdg-open", path])
return 1
@@ -255,7 +258,8 @@ class DisplayViewer(UnixViewer):
deprecate("The 'file' argument", 10, "'path'")
path = options.pop("file")
else:
- raise TypeError("Missing required argument: 'path'")
+ msg = "Missing required argument: 'path'"
+ raise TypeError(msg)
args = ["display"]
title = options.get("title")
if title:
@@ -286,7 +290,8 @@ class GmDisplayViewer(UnixViewer):
deprecate("The 'file' argument", 10, "'path'")
path = options.pop("file")
else:
- raise TypeError("Missing required argument: 'path'")
+ msg = "Missing required argument: 'path'"
+ raise TypeError(msg)
subprocess.Popen(["gm", "display", path])
return 1
@@ -311,7 +316,8 @@ class EogViewer(UnixViewer):
deprecate("The 'file' argument", 10, "'path'")
path = options.pop("file")
else:
- raise TypeError("Missing required argument: 'path'")
+ msg = "Missing required argument: 'path'"
+ raise TypeError(msg)
subprocess.Popen(["eog", "-n", path])
return 1
@@ -342,7 +348,8 @@ class XVViewer(UnixViewer):
deprecate("The 'file' argument", 10, "'path'")
path = options.pop("file")
else:
- raise TypeError("Missing required argument: 'path'")
+ msg = "Missing required argument: 'path'"
+ raise TypeError(msg)
args = ["xv"]
title = options.get("title")
if title:
diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py
index 1baef7db4..b7ebddf06 100644
--- a/src/PIL/ImageStat.py
+++ b/src/PIL/ImageStat.py
@@ -36,7 +36,8 @@ class Stat:
except AttributeError:
self.h = image_or_list # assume it to be a histogram list
if not isinstance(self.h, list):
- raise TypeError("first argument must be image or list")
+ msg = "first argument must be image or list"
+ raise TypeError(msg)
self.bands = list(range(len(self.h) // 256))
def __getattr__(self, id):
diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py
index 949cf1fbf..09a6356fa 100644
--- a/src/PIL/ImageTk.py
+++ b/src/PIL/ImageTk.py
@@ -284,7 +284,8 @@ def _show(image, title):
super().__init__(master, image=self.image, bg="black", bd=0)
if not tkinter._default_root:
- raise OSError("tkinter not initialized")
+ msg = "tkinter not initialized"
+ raise OSError(msg)
top = tkinter.Toplevel()
if title:
top.title(title)
diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index 5790acdaf..cfeadd53c 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -39,15 +39,20 @@ class ImtImageFile(ImageFile.ImageFile):
# Quick rejection: if there's not a LF among the first
# 100 bytes, this is (probably) not a text header.
- if b"\n" not in self.fp.read(100):
- raise SyntaxError("not an IM file")
- self.fp.seek(0)
+ buffer = self.fp.read(100)
+ if b"\n" not in buffer:
+ msg = "not an IM file"
+ raise SyntaxError(msg)
xsize = ysize = 0
while True:
- s = self.fp.read(1)
+ if buffer:
+ s = buffer[:1]
+ buffer = buffer[1:]
+ else:
+ s = self.fp.read(1)
if not s:
break
@@ -55,7 +60,12 @@ class ImtImageFile(ImageFile.ImageFile):
# image data begins
self.tile = [
- ("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))
+ (
+ "raw",
+ (0, 0) + self.size,
+ self.fp.tell() - len(buffer),
+ (self.mode, 0, 1),
+ )
]
break
@@ -63,8 +73,11 @@ class ImtImageFile(ImageFile.ImageFile):
else:
# read key/value pair
- # FIXME: dangerous, may read whole file
- s = s + self.fp.readline()
+ if b"\n" not in buffer:
+ buffer += self.fp.read(100)
+ lines = buffer.split(b"\n")
+ s += lines.pop(0)
+ buffer = b"\n".join(lines)
if len(s) == 1 or len(s) > 100:
break
if s[0] == ord(b"*"):
@@ -74,13 +87,13 @@ class ImtImageFile(ImageFile.ImageFile):
if not m:
break
k, v = m.group(1, 2)
- if k == "width":
+ if k == b"width":
xsize = int(v)
self._size = xsize, ysize
- elif k == "height":
+ elif k == b"height":
ysize = int(v)
self._size = xsize, ysize
- elif k == "pixel" and v == "n8":
+ elif k == b"pixel" and v == b"n8":
self.mode = "L"
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index 0bbe50668..774817569 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -66,12 +66,14 @@ class IptcImageFile(ImageFile.ImageFile):
# syntax
if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9:
- raise SyntaxError("invalid IPTC/NAA file")
+ msg = "invalid IPTC/NAA file"
+ raise SyntaxError(msg)
# field size
size = s[3]
if size > 132:
- raise OSError("illegal field length in IPTC/NAA file")
+ msg = "illegal field length in IPTC/NAA file"
+ raise OSError(msg)
elif size == 128:
size = 0
elif size > 128:
@@ -122,7 +124,8 @@ class IptcImageFile(ImageFile.ImageFile):
try:
compression = COMPRESSION[self.getint((3, 120))]
except KeyError as e:
- raise OSError("Unknown IPTC image compression") from e
+ msg = "Unknown IPTC image compression"
+ raise OSError(msg) from e
# tile
if tag == (8, 10):
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index c67d8d6bf..7457874c1 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -44,13 +44,13 @@ class BoxReader:
def _read_bytes(self, num_bytes):
if not self._can_read(num_bytes):
- raise SyntaxError("Not enough data in header")
+ msg = "Not enough data in header"
+ raise SyntaxError(msg)
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)}."
- )
+ msg = f"Expected to read {num_bytes} bytes but only got {len(data)}."
+ raise OSError(msg)
if self.remaining_in_box > 0:
self.remaining_in_box -= num_bytes
@@ -87,7 +87,8 @@ class BoxReader:
hlen = 8
if lbox < hlen or not self._can_read(lbox - hlen):
- raise SyntaxError("Invalid header length")
+ msg = "Invalid header length"
+ raise SyntaxError(msg)
self.remaining_in_box = lbox - hlen
return tbox
@@ -189,7 +190,8 @@ def _parse_jp2_header(fp):
break
if size is None or mode is None:
- raise SyntaxError("Malformed JP2 header")
+ msg = "Malformed JP2 header"
+ raise SyntaxError(msg)
return size, mode, mimetype, dpi
@@ -217,10 +219,12 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
if dpi is not None:
self.info["dpi"] = dpi
else:
- raise SyntaxError("not a JPEG 2000 file")
+ msg = "not a JPEG 2000 file"
+ raise SyntaxError(msg)
if self.size is None or self.mode is None:
- raise SyntaxError("unable to determine size/mode")
+ msg = "unable to determine size/mode"
+ raise SyntaxError(msg)
self._reduce = 0
self.layers = 0
@@ -312,7 +316,8 @@ def _save(im, fp, filename):
]
)
):
- raise ValueError("quality_layers must be a sequence of numbers")
+ msg = "quality_layers must be a sequence of numbers"
+ raise ValueError(msg)
num_resolutions = info.get("num_resolutions", 0)
cblk_size = info.get("codeblock_size", None)
@@ -321,6 +326,7 @@ def _save(im, fp, filename):
progression = info.get("progression", "LRCP")
cinema_mode = info.get("cinema_mode", "no")
mct = info.get("mct", 0)
+ signed = info.get("signed", False)
fd = -1
if hasattr(fp, "fileno"):
@@ -342,6 +348,7 @@ def _save(im, fp, filename):
progression,
cinema_mode,
mct,
+ signed,
fd,
)
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index a6ed223bc..9657ae9d0 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -45,6 +45,7 @@ from . import Image, ImageFile, TiffImagePlugin
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
+from ._binary import o16be as o16
from ._deprecate import deprecate
from .JpegPresets import presets
@@ -89,6 +90,7 @@ def APP(self, marker):
if "exif" not in self.info:
# extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change
+ self._exif_offset = self.fp.tell() - n + 6
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
self.info["flashpix"] = s # FIXME: value will change
@@ -202,7 +204,8 @@ def SOF(self, marker):
self.bits = s[0]
if self.bits != 8:
- raise SyntaxError(f"cannot handle {self.bits}-bit layers")
+ msg = f"cannot handle {self.bits}-bit layers"
+ raise SyntaxError(msg)
self.layers = s[5]
if self.layers == 1:
@@ -212,7 +215,8 @@ def SOF(self, marker):
elif self.layers == 4:
self.mode = "CMYK"
else:
- raise SyntaxError(f"cannot handle {self.layers}-layer images")
+ msg = f"cannot handle {self.layers}-layer images"
+ raise SyntaxError(msg)
if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]:
self.info["progressive"] = self.info["progression"] = 1
@@ -251,7 +255,8 @@ def DQT(self, marker):
precision = 1 if (v // 16 == 0) else 2 # in bytes
qt_length = 1 + precision * 64
if len(s) < qt_length:
- raise SyntaxError("bad quantization table marker")
+ msg = "bad quantization table marker"
+ raise SyntaxError(msg)
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
@@ -348,7 +353,8 @@ class JpegImageFile(ImageFile.ImageFile):
s = self.fp.read(3)
if not _accept(s):
- raise SyntaxError("not a JPEG file")
+ msg = "not a JPEG file"
+ raise SyntaxError(msg)
s = b"\xFF"
# Create attributes
@@ -392,7 +398,8 @@ class JpegImageFile(ImageFile.ImageFile):
elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
s = self.fp.read(1)
else:
- raise SyntaxError("no marker found")
+ msg = "no marker found"
+ raise SyntaxError(msg)
def load_read(self, read_bytes):
"""
@@ -456,7 +463,8 @@ class JpegImageFile(ImageFile.ImageFile):
if os.path.exists(self.filename):
subprocess.check_call(["djpeg", "-outfile", path, self.filename])
else:
- raise ValueError("Invalid Filename")
+ msg = "Invalid Filename"
+ raise ValueError(msg)
try:
with Image.open(path) as _im:
@@ -522,12 +530,14 @@ def _getmp(self):
info.load(file_contents)
mp = dict(info)
except Exception as e:
- raise SyntaxError("malformed MP Index (unreadable directory)") from e
+ msg = "malformed MP Index (unreadable directory)"
+ raise SyntaxError(msg) from e
# it's an error not to have a number of images
try:
quant = mp[0xB001]
except KeyError as e:
- raise SyntaxError("malformed MP Index (no number of images)") from e
+ msg = "malformed MP Index (no number of images)"
+ raise SyntaxError(msg) from e
# get MP entries
mpentries = []
try:
@@ -549,7 +559,8 @@ def _getmp(self):
if mpentryattr["ImageDataFormat"] == 0:
mpentryattr["ImageDataFormat"] = "JPEG"
else:
- raise SyntaxError("unsupported picture format in MPO")
+ msg = "unsupported picture format in MPO"
+ raise SyntaxError(msg)
mptypemap = {
0x000000: "Undefined",
0x010001: "Large Thumbnail (VGA Equivalent)",
@@ -564,7 +575,8 @@ def _getmp(self):
mpentries.append(mpentry)
mp[0xB002] = mpentries
except KeyError as e:
- raise SyntaxError("malformed MP Index (bad MP Entry)") from e
+ msg = "malformed MP Index (bad MP Entry)"
+ raise SyntaxError(msg) from e
# Next we should try and parse the individual image unique ID list;
# we don't because I've never seen this actually used in a real MPO
# file and so can't test it.
@@ -624,12 +636,14 @@ def get_sampling(im):
def _save(im, fp, filename):
if im.width == 0 or im.height == 0:
- raise ValueError("cannot write empty image as JPEG")
+ msg = "cannot write empty image as JPEG"
+ raise ValueError(msg)
try:
rawmode = RAWMODE[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as JPEG") from e
+ msg = f"cannot write mode {im.mode} as JPEG"
+ raise OSError(msg) from e
info = im.encoderinfo
@@ -649,7 +663,8 @@ def _save(im, fp, filename):
subsampling = preset.get("subsampling", -1)
qtables = preset.get("quantization")
elif not isinstance(quality, int):
- raise ValueError("Invalid quality setting")
+ msg = "Invalid quality setting"
+ raise ValueError(msg)
else:
if subsampling in presets:
subsampling = presets[subsampling].get("subsampling", -1)
@@ -668,7 +683,8 @@ def _save(im, fp, filename):
subsampling = 2
elif subsampling == "keep":
if im.format != "JPEG":
- raise ValueError("Cannot use 'keep' when original image is not a JPEG")
+ msg = "Cannot use 'keep' when original image is not a JPEG"
+ raise ValueError(msg)
subsampling = get_sampling(im)
def validate_qtables(qtables):
@@ -682,7 +698,8 @@ def _save(im, fp, filename):
for num in line.split("#", 1)[0].split()
]
except ValueError as e:
- raise ValueError("Invalid quantization table") from e
+ msg = "Invalid quantization table"
+ raise ValueError(msg) from e
else:
qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)]
if isinstance(qtables, (tuple, list, dict)):
@@ -693,21 +710,24 @@ def _save(im, fp, filename):
elif isinstance(qtables, tuple):
qtables = list(qtables)
if not (0 < len(qtables) < 5):
- raise ValueError("None or too many quantization tables")
+ msg = "None or too many quantization tables"
+ raise ValueError(msg)
for idx, table in enumerate(qtables):
try:
if len(table) != 64:
raise TypeError
table = array.array("H", table)
except TypeError as e:
- raise ValueError("Invalid quantization table") from e
+ msg = "Invalid quantization table"
+ raise ValueError(msg) from e
else:
qtables[idx] = list(table)
return qtables
if qtables == "keep":
if im.format != "JPEG":
- raise ValueError("Cannot use 'keep' when original image is not a JPEG")
+ msg = "Cannot use 'keep' when original image is not a JPEG"
+ raise ValueError(msg)
qtables = getattr(im, "quantization", None)
qtables = validate_qtables(qtables)
@@ -724,7 +744,7 @@ def _save(im, fp, filename):
icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:]
i = 1
for marker in markers:
- size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker))
+ size = o16(2 + ICC_OVERHEAD_LEN + len(marker))
extra += (
b"\xFF\xE2"
+ size
@@ -735,6 +755,8 @@ def _save(im, fp, filename):
)
i += 1
+ comment = info.get("comment", im.info.get("comment"))
+
# "progressive" is the official name, but older documentation
# says "progression"
# FIXME: issue a warning if the wrong form is used (post-1.1.7)
@@ -757,6 +779,7 @@ def _save(im, fp, filename):
dpi[1],
subsampling,
qtables,
+ comment,
extra,
exif,
)
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index cd047fe9d..8d4d826aa 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -39,7 +39,8 @@ class McIdasImageFile(ImageFile.ImageFile):
# parse area file directory
s = self.fp.read(256)
if not _accept(s) or len(s) != 256:
- raise SyntaxError("not an McIdas area file")
+ msg = "not an McIdas area file"
+ raise SyntaxError(msg)
self.area_descriptor_raw = s
self.area_descriptor = w = [0] + list(struct.unpack("!64i", s))
@@ -56,7 +57,8 @@ class McIdasImageFile(ImageFile.ImageFile):
mode = "I"
rawmode = "I;32B"
else:
- raise SyntaxError("unsupported McIdas format")
+ msg = "unsupported McIdas format"
+ raise SyntaxError(msg)
self.mode = mode
self._size = w[10], w[9]
diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py
index d4f6c90f7..e7e1054a3 100644
--- a/src/PIL/MicImagePlugin.py
+++ b/src/PIL/MicImagePlugin.py
@@ -47,7 +47,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
try:
self.ole = olefile.OleFileIO(self.fp)
except OSError as e:
- raise SyntaxError("not an MIC file; invalid OLE file") from e
+ msg = "not an MIC file; invalid OLE file"
+ raise SyntaxError(msg) from e
# find ACI subfiles with Image members (maybe not the
# best way to identify MIC files, but what the... ;-)
@@ -60,7 +61,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
# if we didn't find any images, this is probably not
# an MIC file.
if not self.images:
- raise SyntaxError("not an MIC file; no image entries")
+ msg = "not an MIC file; no image entries"
+ raise SyntaxError(msg)
self.frame = None
self._n_frames = len(self.images)
@@ -77,7 +79,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
try:
filename = self.images[frame]
except IndexError as e:
- raise EOFError("no such frame") from e
+ msg = "no such frame"
+ raise EOFError(msg) from e
self.fp = self.ole.openstream(filename)
diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py
index a358dfdce..2d799d6d8 100644
--- a/src/PIL/MpegImagePlugin.py
+++ b/src/PIL/MpegImagePlugin.py
@@ -67,7 +67,8 @@ class MpegImageFile(ImageFile.ImageFile):
s = BitStream(self.fp)
if s.read(32) != 0x1B3:
- raise SyntaxError("not an MPEG file")
+ msg = "not an MPEG file"
+ raise SyntaxError(msg)
self.mode = "RGB"
self._size = s.read(12), s.read(12)
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 5bfd8efc1..b1ec2c7bc 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -22,7 +22,14 @@ import itertools
import os
import struct
-from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
+from . import (
+ ExifTags,
+ Image,
+ ImageFile,
+ ImageSequence,
+ JpegImagePlugin,
+ TiffImagePlugin,
+)
from ._binary import i16be as i16
from ._binary import o32le
@@ -45,14 +52,22 @@ def _save_all(im, fp, filename):
_save(im, fp, filename)
return
+ mpf_offset = 28
offsets = []
for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence):
if not offsets:
# APP2 marker
- im.encoderinfo["extra"] = (
- b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70
+ im_frame.encoderinfo["extra"] = (
+ b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82
)
+ exif = im_frame.encoderinfo.get("exif")
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes()
+ im_frame.encoderinfo["exif"] = exif
+ if exif:
+ mpf_offset += 4 + len(exif)
+
JpegImagePlugin._save(im_frame, fp, filename)
offsets.append(fp.tell())
else:
@@ -60,6 +75,7 @@ def _save_all(im, fp, filename):
offsets.append(fp.tell() - offsets[-1])
ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[0xB000] = b"0100"
ifd[0xB001] = len(offsets)
mpentries = b""
@@ -71,11 +87,11 @@ def _save_all(im, fp, filename):
mptype = 0x000000 # Undefined
mpentries += struct.pack(" 100:
- raise SyntaxError("bad palette file")
+ msg = "bad palette file"
+ raise SyntaxError(msg)
v = [int(x) for x in s.split()]
try:
diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py
index 700f10e3f..109aad9ab 100644
--- a/src/PIL/PalmImagePlugin.py
+++ b/src/PIL/PalmImagePlugin.py
@@ -138,7 +138,8 @@ def _save(im, fp, filename):
bpp = im.info["bpp"]
im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval))
else:
- raise OSError(f"cannot write mode {im.mode} as Palm")
+ msg = f"cannot write mode {im.mode} as Palm"
+ raise OSError(msg)
# we ignore the palette here
im.mode = "P"
@@ -154,7 +155,8 @@ def _save(im, fp, filename):
else:
- raise OSError(f"cannot write mode {im.mode} as Palm")
+ msg = f"cannot write mode {im.mode} as Palm"
+ raise OSError(msg)
#
# make sure image data is available
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index 38caf5c63..5802d386a 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -35,7 +35,8 @@ class PcdImageFile(ImageFile.ImageFile):
s = self.fp.read(2048)
if s[:4] != b"PCD_":
- raise SyntaxError("not a PCD file")
+ msg = "not a PCD file"
+ raise SyntaxError(msg)
orientation = s[1538] & 3
self.tile_post_rotate = None
diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py
index 442ac70c4..ecce1b097 100644
--- a/src/PIL/PcfFontFile.py
+++ b/src/PIL/PcfFontFile.py
@@ -63,7 +63,8 @@ class PcfFontFile(FontFile.FontFile):
magic = l32(fp.read(4))
if magic != PCF_MAGIC:
- raise SyntaxError("not a PCF file")
+ msg = "not a PCF file"
+ raise SyntaxError(msg)
super().__init__()
@@ -186,7 +187,8 @@ class PcfFontFile(FontFile.FontFile):
nbitmaps = i32(fp.read(4))
if nbitmaps != len(metrics):
- raise OSError("Wrong number of bitmaps")
+ msg = "Wrong number of bitmaps"
+ raise OSError(msg)
offsets = []
for i in range(nbitmaps):
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index 841c18a22..3202475dc 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -54,12 +54,14 @@ class PcxImageFile(ImageFile.ImageFile):
# header
s = self.fp.read(128)
if not _accept(s):
- raise SyntaxError("not a PCX file")
+ msg = "not a PCX file"
+ raise SyntaxError(msg)
# image
bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1
if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]:
- raise SyntaxError("bad PCX image size")
+ msg = "bad PCX image size"
+ raise SyntaxError(msg)
logger.debug("BBox: %s %s %s %s", *bbox)
# format
@@ -105,7 +107,8 @@ class PcxImageFile(ImageFile.ImageFile):
rawmode = "RGB;L"
else:
- raise OSError("unknown PCX mode")
+ msg = "unknown PCX mode"
+ raise OSError(msg)
self.mode = mode
self._size = bbox[2] - bbox[0], bbox[3] - bbox[1]
@@ -144,7 +147,8 @@ def _save(im, fp, filename):
try:
version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e:
- raise ValueError(f"Cannot save {im.mode} images as PCX") from e
+ msg = f"Cannot save {im.mode} images as PCX"
+ raise ValueError(msg) from e
# bytes per plane
stride = (im.size[0] * bits + 7) // 8
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
index 404759a7f..baad4939f 100644
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -174,7 +174,8 @@ def _save(im, fp, filename, save_all=False):
procset = "ImageC" # color images
decode = [1, 0, 1, 0, 1, 0, 1, 0]
else:
- raise ValueError(f"cannot save mode {im.mode}")
+ msg = f"cannot save mode {im.mode}"
+ raise ValueError(msg)
#
# image
@@ -198,7 +199,8 @@ def _save(im, fp, filename, save_all=False):
elif filter == "RunLengthDecode":
ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)])
else:
- raise ValueError(f"unsupported PDF filter ({filter})")
+ msg = f"unsupported PDF filter ({filter})"
+ raise ValueError(msg)
stream = op.getvalue()
if filter == "CCITTFaxDecode":
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index fd5cc5a61..aa5ea2fbb 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -138,9 +138,10 @@ class XrefTable:
elif key in self.deleted_entries:
generation = self.deleted_entries[key]
else:
- raise IndexError(
+ msg = (
"object ID " + str(key) + " cannot be deleted because it doesn't exist"
)
+ raise IndexError(msg)
def __contains__(self, key):
return key in self.existing_entries or key in self.new_entries
@@ -314,9 +315,8 @@ class PdfStream:
expected_length = self.dictionary.Length
return zlib.decompress(self.buf, bufsize=int(expected_length))
else:
- raise NotImplementedError(
- f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
- )
+ msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
+ raise NotImplementedError(msg)
def pdf_repr(x):
@@ -358,7 +358,8 @@ class PdfParser:
def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"):
if buf and f:
- raise RuntimeError("specify buf or f or filename, but not both buf and f")
+ msg = "specify buf or f or filename, but not both buf and f"
+ raise RuntimeError(msg)
self.filename = filename
self.buf = buf
self.f = f
@@ -816,10 +817,10 @@ class PdfParser:
try:
stream_len = int(result[b"Length"])
except (TypeError, KeyError, ValueError) as e:
- raise PdfFormatError(
- "bad or missing Length in stream dict (%r)"
- % result.get(b"Length", None)
- ) from e
+ msg = "bad or missing Length in stream dict (%r)" % result.get(
+ b"Length", None
+ )
+ raise PdfFormatError(msg) from e
stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len)
check_format_condition(m, "stream end not found")
@@ -873,7 +874,8 @@ class PdfParser:
if m:
return cls.get_literal_string(data, m.end())
# return None, offset # fallback (only for debugging)
- raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32]))
+ msg = "unrecognized object: " + repr(data[offset : offset + 32])
+ raise PdfFormatError(msg)
re_lit_str_token = re.compile(
rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
@@ -920,7 +922,8 @@ class PdfParser:
result.extend(b")")
nesting_depth -= 1
offset = m.end()
- raise PdfFormatError("unfinished literal string")
+ msg = "unfinished literal string"
+ raise PdfFormatError(msg)
re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline)
re_xref_subsection_start = re.compile(
diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py
index c4860b6c4..8d0a34dba 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -44,7 +44,8 @@ class PixarImageFile(ImageFile.ImageFile):
# assuming a 4-byte magic label
s = self.fp.read(4)
if not _accept(s):
- raise SyntaxError("not a PIXAR file")
+ msg = "not a PIXAR file"
+ raise SyntaxError(msg)
# read rest of header
s = s + self.fp.read(508)
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 442c65e6f..b6626bbc5 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -138,14 +138,16 @@ def __getattr__(name):
if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[name]
- raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
def _safe_zlib_decompress(s):
dobj = zlib.decompressobj()
plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
if dobj.unconsumed_tail:
- raise ValueError("Decompressed Data Too Large")
+ msg = "Decompressed Data Too Large"
+ raise ValueError(msg)
return plaintext
@@ -178,7 +180,8 @@ class ChunkStream:
if not is_cid(cid):
if not ImageFile.LOAD_TRUNCATED_IMAGES:
- raise SyntaxError(f"broken PNG file (chunk {repr(cid)})")
+ msg = f"broken PNG file (chunk {repr(cid)})"
+ raise SyntaxError(msg)
return cid, pos, length
@@ -189,7 +192,7 @@ class ChunkStream:
self.close()
def close(self):
- self.queue = self.crc = self.fp = None
+ self.queue = self.fp = None
def push(self, cid, pos, length):
@@ -215,16 +218,14 @@ class ChunkStream:
crc1 = _crc32(data, _crc32(cid))
crc2 = i32(self.fp.read(4))
if crc1 != crc2:
- raise SyntaxError(
- f"broken PNG file (bad header checksum in {repr(cid)})"
- )
+ msg = f"broken PNG file (bad header checksum in {repr(cid)})"
+ raise SyntaxError(msg)
except struct.error as e:
- raise SyntaxError(
- f"broken PNG file (incomplete checksum in {repr(cid)})"
- ) from e
+ msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
+ raise SyntaxError(msg) from e
def crc_skip(self, cid, data):
- """Read checksum. Used if the C module is not present"""
+ """Read checksum"""
self.fp.read(4)
@@ -239,7 +240,8 @@ class ChunkStream:
try:
cid, pos, length = self.read()
except struct.error as e:
- raise OSError("truncated PNG file") from e
+ msg = "truncated PNG file"
+ raise OSError(msg) from e
if cid == endchunk:
break
@@ -376,10 +378,11 @@ class PngStream(ChunkStream):
def check_text_memory(self, chunklen):
self.text_memory += chunklen
if self.text_memory > MAX_TEXT_MEMORY:
- raise ValueError(
+ msg = (
"Too much memory used in text chunks: "
f"{self.text_memory}>MAX_TEXT_MEMORY"
)
+ raise ValueError(msg)
def save_rewind(self):
self.rewind_state = {
@@ -407,7 +410,8 @@ class PngStream(ChunkStream):
logger.debug("Compression method %s", s[i])
comp_method = s[i]
if comp_method != 0:
- raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk")
+ msg = f"Unknown compression method {comp_method} in iCCP chunk"
+ raise SyntaxError(msg)
try:
icc_profile = _safe_zlib_decompress(s[i + 2 :])
except ValueError:
@@ -427,7 +431,8 @@ class PngStream(ChunkStream):
if length < 13:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("Truncated IHDR chunk")
+ msg = "Truncated IHDR chunk"
+ raise ValueError(msg)
self.im_size = i32(s, 0), i32(s, 4)
try:
self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
@@ -436,7 +441,8 @@ class PngStream(ChunkStream):
if s[12]:
self.im_info["interlace"] = 1
if s[11]:
- raise SyntaxError("unknown filter category")
+ msg = "unknown filter category"
+ raise SyntaxError(msg)
return s
def chunk_IDAT(self, pos, length):
@@ -512,7 +518,8 @@ class PngStream(ChunkStream):
if length < 1:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("Truncated sRGB chunk")
+ msg = "Truncated sRGB chunk"
+ raise ValueError(msg)
self.im_info["srgb"] = s[0]
return s
@@ -523,7 +530,8 @@ class PngStream(ChunkStream):
if length < 9:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("Truncated pHYs chunk")
+ msg = "Truncated pHYs chunk"
+ raise ValueError(msg)
px, py = i32(s, 0), i32(s, 4)
unit = s[8]
if unit == 1: # meter
@@ -567,7 +575,8 @@ class PngStream(ChunkStream):
else:
comp_method = 0
if comp_method != 0:
- raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk")
+ msg = f"Unknown compression method {comp_method} in zTXt chunk"
+ raise SyntaxError(msg)
try:
v = _safe_zlib_decompress(v[1:])
except ValueError:
@@ -639,7 +648,8 @@ class PngStream(ChunkStream):
if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("APNG contains truncated acTL chunk")
+ msg = "APNG contains truncated acTL chunk"
+ raise ValueError(msg)
if self.im_n_frames is not None:
self.im_n_frames = None
warnings.warn("Invalid APNG, will use default PNG image if possible")
@@ -658,18 +668,21 @@ class PngStream(ChunkStream):
if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES:
return s
- raise ValueError("APNG contains truncated fcTL chunk")
+ msg = "APNG contains truncated fcTL chunk"
+ raise ValueError(msg)
seq = i32(s)
if (self._seq_num is None and seq != 0) or (
self._seq_num is not None and self._seq_num != seq - 1
):
- raise SyntaxError("APNG contains frame sequence errors")
+ msg = "APNG contains frame sequence errors"
+ raise SyntaxError(msg)
self._seq_num = seq
width, height = i32(s, 4), i32(s, 8)
px, py = i32(s, 12), i32(s, 16)
im_w, im_h = self.im_size
if px + width > im_w or py + height > im_h:
- raise SyntaxError("APNG contains invalid frames")
+ msg = "APNG contains invalid frames"
+ raise SyntaxError(msg)
self.im_info["bbox"] = (px, py, px + width, py + height)
delay_num, delay_den = i16(s, 20), i16(s, 22)
if delay_den == 0:
@@ -684,11 +697,13 @@ class PngStream(ChunkStream):
if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length)
return s
- raise ValueError("APNG contains truncated fDAT chunk")
+ msg = "APNG contains truncated fDAT chunk"
+ raise ValueError(msg)
s = ImageFile._safe_read(self.fp, 4)
seq = i32(s)
if self._seq_num != seq - 1:
- raise SyntaxError("APNG contains frame sequence errors")
+ msg = "APNG contains frame sequence errors"
+ raise SyntaxError(msg)
self._seq_num = seq
return self.chunk_IDAT(pos + 4, length - 4)
@@ -713,7 +728,8 @@ class PngImageFile(ImageFile.ImageFile):
def _open(self):
if not _accept(self.fp.read(8)):
- raise SyntaxError("not a PNG file")
+ msg = "not a PNG file"
+ raise SyntaxError(msg)
self._fp = self.fp
self.__frame = 0
@@ -797,7 +813,8 @@ class PngImageFile(ImageFile.ImageFile):
"""Verify PNG file"""
if self.fp is None:
- raise RuntimeError("verify must be called directly after open")
+ msg = "verify must be called directly after open"
+ raise RuntimeError(msg)
# back up to beginning of IDAT block
self.fp.seek(self.tile[0][2] - 8)
@@ -821,7 +838,8 @@ class PngImageFile(ImageFile.ImageFile):
self._seek(f)
except EOFError as e:
self.seek(last_frame)
- raise EOFError("no more images in APNG file") from e
+ msg = "no more images in APNG file"
+ raise EOFError(msg) from e
def _seek(self, frame, rewind=False):
if frame == 0:
@@ -844,7 +862,8 @@ class PngImageFile(ImageFile.ImageFile):
self.__frame = 0
else:
if frame != self.__frame + 1:
- raise ValueError(f"cannot seek to frame {frame}")
+ msg = f"cannot seek to frame {frame}"
+ raise ValueError(msg)
# ensure previous frame was loaded
self.load()
@@ -869,11 +888,13 @@ class PngImageFile(ImageFile.ImageFile):
break
if cid == b"IEND":
- raise EOFError("No more images in APNG file")
+ msg = "No more images in APNG file"
+ raise EOFError(msg)
if cid == b"fcTL":
if frame_start:
# there must be at least one fdAT chunk between fcTL chunks
- raise SyntaxError("APNG missing frame data")
+ msg = "APNG missing frame data"
+ raise SyntaxError(msg)
frame_start = True
try:
@@ -1089,28 +1110,28 @@ class _fdat:
self.seq_num += 1
-def _write_multiple_frames(im, fp, chunk, rawmode):
- default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
+def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
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", Disposal.OP_NONE))
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
if default_image:
- chain = itertools.chain(im.encoderinfo.get("append_images", []))
+ chain = itertools.chain(append_images)
else:
- chain = itertools.chain([im], im.encoderinfo.get("append_images", []))
+ chain = itertools.chain([im], append_images)
im_frames = []
frame_count = 0
for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq):
- im_frame = im_frame.copy()
- if im_frame.mode != im.mode:
- if im.mode == "P":
- im_frame = im_frame.convert(im.mode, palette=im.palette)
+ if im_frame.mode == rawmode:
+ im_frame = im_frame.copy()
+ else:
+ if rawmode == "P":
+ im_frame = im_frame.convert(rawmode, palette=im.palette)
else:
- im_frame = im_frame.convert(im.mode)
+ im_frame = im_frame.convert(rawmode)
encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
@@ -1128,7 +1149,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode):
prev_disposal = Disposal.OP_BACKGROUND
if prev_disposal == Disposal.OP_BACKGROUND:
- base_im = previous["im"]
+ base_im = previous["im"].copy()
dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
bbox = previous["bbox"]
if bbox:
@@ -1221,7 +1242,26 @@ def _save_all(im, fp, filename):
def _save(im, fp, filename, chunk=putchunk, save_all=False):
# save an image to disk (called by the save method)
- mode = im.mode
+ if save_all:
+ default_image = im.encoderinfo.get(
+ "default_image", im.info.get("default_image")
+ )
+ modes = set()
+ append_images = im.encoderinfo.get("append_images", [])
+ if default_image:
+ chain = itertools.chain(append_images)
+ else:
+ chain = itertools.chain([im], append_images)
+ for im_seq in chain:
+ for im_frame in ImageSequence.Iterator(im_seq):
+ modes.add(im_frame.mode)
+ for mode in ("RGBA", "RGB", "P"):
+ if mode in modes:
+ break
+ else:
+ mode = modes.pop()
+ else:
+ mode = im.mode
if mode == "P":
@@ -1258,7 +1298,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
try:
rawmode, mode = _OUTMODES[mode]
except KeyError as e:
- raise OSError(f"cannot write mode {mode} as PNG") from e
+ msg = f"cannot write mode {mode} as PNG"
+ raise OSError(msg) from e
#
# write minimal PNG file
@@ -1339,7 +1380,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
if "transparency" in im.encoderinfo:
# don't bother with transparency if it's an RGBA
# and it's in the info dict. It's probably just stale.
- raise OSError("cannot use transparency for this mode")
+ msg = "cannot use transparency for this mode"
+ raise OSError(msg)
else:
if im.mode == "P" and im.im.getpalettemode() == "RGBA":
alpha = im.im.getpalette("RGBA", "A")
@@ -1364,7 +1406,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunks.remove(cid)
chunk(fp, cid, data)
- exif = im.encoderinfo.get("exif", im.info.get("exif"))
+ exif = im.encoderinfo.get("exif")
if exif:
if isinstance(exif, Image.Exif):
exif = exif.tobytes(8)
@@ -1373,7 +1415,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, b"eXIf", exif)
if save_all:
- _write_multiple_frames(im, fp, chunk, rawmode)
+ _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
else:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 392771d3e..dee2f1e15 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -84,9 +84,11 @@ class PpmImageFile(ImageFile.ImageFile):
token += c
if not token:
# Token was not even 1 byte
- raise ValueError("Reached EOF while reading header")
+ msg = "Reached EOF while reading header"
+ raise ValueError(msg)
elif len(token) > 10:
- raise ValueError(f"Token too long in file header: {token.decode()}")
+ msg = f"Token too long in file header: {token.decode()}"
+ raise ValueError(msg)
return token
def _open(self):
@@ -94,7 +96,8 @@ class PpmImageFile(ImageFile.ImageFile):
try:
mode = MODES[magic_number]
except KeyError:
- raise SyntaxError("not a PPM file")
+ msg = "not a PPM file"
+ raise SyntaxError(msg)
if magic_number in (b"P1", b"P4"):
self.custom_mimetype = "image/x-portable-bitmap"
@@ -122,9 +125,8 @@ class PpmImageFile(ImageFile.ImageFile):
elif ix == 2: # token is maxval
maxval = token
if not 0 < maxval < 65536:
- raise ValueError(
- "maxval must be greater than 0 and less than 65536"
- )
+ msg = "maxval must be greater than 0 and less than 65536"
+ raise ValueError(msg)
if maxval > 255 and mode == "L":
self.mode = "I"
@@ -208,7 +210,8 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
tokens = b"".join(block.split())
for token in tokens:
if token not in (48, 49):
- raise ValueError(f"Invalid token for this mode: {bytes([token])}")
+ msg = b"Invalid token for this mode: %s" % bytes([token])
+ raise ValueError(msg)
data = (data + tokens)[:total_bytes]
invert = bytes.maketrans(b"01", b"\xFF\x00")
return data.translate(invert)
@@ -241,18 +244,19 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
if block and not block[-1:].isspace(): # block might split token
half_token = tokens.pop() # save half token for later
if len(half_token) > max_len: # prevent buildup of half_token
- raise ValueError(
- f"Token too long found in data: {half_token[:max_len + 1]}"
+ msg = (
+ b"Token too long found in data: %s" % half_token[: max_len + 1]
)
+ raise ValueError(msg)
for token in tokens:
if len(token) > max_len:
- raise ValueError(
- f"Token too long found in data: {token[:max_len + 1]}"
- )
+ msg = b"Token too long found in data: %s" % token[: max_len + 1]
+ raise ValueError(msg)
value = int(token)
if value > maxval:
- raise ValueError(f"Channel value too large for this mode: {value}")
+ msg = f"Channel value too large for this mode: {value}"
+ raise ValueError(msg)
value = round(value / maxval * out_max)
data += o32(value) if self.mode == "I" else o8(value)
if len(data) == total_bytes: # finished!
@@ -312,7 +316,8 @@ def _save(im, fp, filename):
elif im.mode in ("RGB", "RGBA"):
rawmode, head = "RGB", b"P6"
else:
- raise OSError(f"cannot write mode {im.mode} as PPM")
+ msg = f"cannot write mode {im.mode} as PPM"
+ raise OSError(msg)
fp.write(head + b"\n%d %d\n" % im.size)
if head == b"P6":
fp.write(b"255\n")
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index bd10e3b95..c1ca30a03 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -65,7 +65,8 @@ class PsdImageFile(ImageFile.ImageFile):
s = read(26)
if not _accept(s) or i16(s, 4) != 1:
- raise SyntaxError("not a PSD file")
+ msg = "not a PSD file"
+ raise SyntaxError(msg)
psd_bits = i16(s, 22)
psd_channels = i16(s, 12)
@@ -74,7 +75,8 @@ class PsdImageFile(ImageFile.ImageFile):
mode, channels = MODES[(psd_mode, psd_bits)]
if channels > psd_channels:
- raise OSError("not enough channels")
+ msg = "not enough channels"
+ raise OSError(msg)
if mode == "RGB" and psd_channels == 4:
mode = "RGBA"
channels = 4
@@ -152,7 +154,8 @@ class PsdImageFile(ImageFile.ImageFile):
self.fp = self._fp
return name, bbox
except IndexError as e:
- raise EOFError("no such layer") from e
+ msg = "no such layer"
+ raise EOFError(msg) from e
def tell(self):
# return layer number (0=image, 1..max=layers)
@@ -170,7 +173,8 @@ def _layerinfo(fp, ct_bytes):
# sanity check
if ct_bytes < (abs(ct) * 20):
- raise SyntaxError("Layer block too short for number of layers requested")
+ msg = "Layer block too short for number of layers requested"
+ raise SyntaxError(msg)
for _ in range(abs(ct)):
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
index 9a2ec48fc..e9cb34ced 100644
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -13,8 +13,7 @@
# Notes:
#
-# * Implements the pixel access object following Access.
-# * Does not implement the line functions, as they don't appear to be used
+# * Implements the pixel access object following Access.c
# * Taking only the tuple form, which is used from python.
# * Fill.c uses the integer form, but it's still going to use the old
# Access.c implementation.
@@ -80,7 +79,8 @@ class PyAccess:
:param color: The pixel value.
"""
if self.readonly:
- raise ValueError("Attempt to putpixel a read only image")
+ msg = "Attempt to putpixel a read only image"
+ raise ValueError(msg)
(x, y) = xy
if x < 0:
x = self.xsize + x
@@ -128,7 +128,8 @@ class PyAccess:
def check_xy(self, xy):
(x, y) = xy
if not (0 <= x < self.xsize and 0 <= y < self.ysize):
- raise ValueError("pixel location out of range")
+ msg = "pixel location out of range"
+ raise ValueError(msg)
return xy
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index f0207bb77..d533c55e5 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -60,7 +60,8 @@ class SgiImageFile(ImageFile.ImageFile):
s = self.fp.read(headlen)
if not _accept(s):
- raise ValueError("Not an SGI image file")
+ msg = "Not an SGI image file"
+ raise ValueError(msg)
# compression : verbatim or RLE
compression = s[2]
@@ -91,7 +92,8 @@ class SgiImageFile(ImageFile.ImageFile):
pass
if rawmode == "":
- raise ValueError("Unsupported SGI image mode")
+ msg = "Unsupported SGI image mode"
+ raise ValueError(msg)
self._size = xsize, ysize
self.mode = rawmode.split(";")[0]
@@ -124,7 +126,8 @@ class SgiImageFile(ImageFile.ImageFile):
def _save(im, fp, filename):
if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L":
- raise ValueError("Unsupported SGI image mode")
+ msg = "Unsupported SGI image mode"
+ raise ValueError(msg)
# Get the keyword arguments
info = im.encoderinfo
@@ -133,7 +136,8 @@ def _save(im, fp, filename):
bpc = info.get("bpc", 1)
if bpc not in (1, 2):
- raise ValueError("Unsupported number of bytes per pixel")
+ msg = "Unsupported number of bytes per pixel"
+ raise ValueError(msg)
# Flip the image, since the origin of SGI file is the bottom-left corner
orientation = -1
@@ -158,9 +162,8 @@ def _save(im, fp, filename):
# assert we've got the right number of bands.
if len(im.getbands()) != z:
- raise ValueError(
- f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}"
- )
+ msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}"
+ raise ValueError(msg)
# Minimum Byte value
pinmin = 0
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index acafc320e..1192c2d73 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -110,14 +110,17 @@ class SpiderImageFile(ImageFile.ImageFile):
t = struct.unpack("<27f", f) # little-endian
hdrlen = isSpiderHeader(t)
if hdrlen == 0:
- raise SyntaxError("not a valid Spider file")
+ msg = "not a valid Spider file"
+ raise SyntaxError(msg)
except struct.error as e:
- raise SyntaxError("not a valid Spider file") from e
+ msg = "not a valid Spider file"
+ raise SyntaxError(msg) from e
h = (99,) + t # add 1 value : spider header index starts at 1
iform = int(h[5])
if iform != 1:
- raise SyntaxError("not a Spider 2D image")
+ msg = "not a Spider 2D image"
+ raise SyntaxError(msg)
self._size = int(h[12]), int(h[2]) # size in pixels (width, height)
self.istack = int(h[24])
@@ -140,7 +143,8 @@ class SpiderImageFile(ImageFile.ImageFile):
offset = hdrlen + self.stkoffset
self.istack = 2 # So Image knows it's still a stack
else:
- raise SyntaxError("inconsistent stack header values")
+ msg = "inconsistent stack header values"
+ raise SyntaxError(msg)
if self.bigendian:
self.rawmode = "F;32BF"
@@ -168,7 +172,8 @@ class SpiderImageFile(ImageFile.ImageFile):
def seek(self, frame):
if self.istack == 0:
- raise EOFError("attempt to seek in a non-stack file")
+ msg = "attempt to seek in a non-stack file"
+ raise EOFError(msg)
if not self._seek_check(frame):
return
self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
@@ -260,7 +265,8 @@ def _save(im, fp, filename):
hdr = makeSpiderHeader(im)
if len(hdr) < 256:
- raise OSError("Error creating Spider header")
+ msg = "Error creating Spider header"
+ raise OSError(msg)
# write the SPIDER header
fp.writelines(hdr)
diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py
index c03759a01..c64de4444 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -54,7 +54,8 @@ class SunImageFile(ImageFile.ImageFile):
# HEAD
s = self.fp.read(32)
if not _accept(s):
- raise SyntaxError("not an SUN raster file")
+ msg = "not an SUN raster file"
+ raise SyntaxError(msg)
offset = 32
@@ -83,14 +84,17 @@ class SunImageFile(ImageFile.ImageFile):
else:
self.mode, rawmode = "RGB", "BGRX"
else:
- raise SyntaxError("Unsupported Mode/Bit Depth")
+ msg = "Unsupported Mode/Bit Depth"
+ raise SyntaxError(msg)
if palette_length:
if palette_length > 1024:
- raise SyntaxError("Unsupported Color Palette Length")
+ msg = "Unsupported Color Palette Length"
+ raise SyntaxError(msg)
if palette_type != 1:
- raise SyntaxError("Unsupported Palette Type")
+ msg = "Unsupported Palette Type"
+ raise SyntaxError(msg)
offset = offset + palette_length
self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length))
@@ -124,7 +128,8 @@ class SunImageFile(ImageFile.ImageFile):
elif file_type == 2:
self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)]
else:
- raise SyntaxError("Unsupported Sun Raster file type")
+ msg = "Unsupported Sun Raster file type"
+ raise SyntaxError(msg)
#
diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py
index d108362fc..20e8a083f 100644
--- a/src/PIL/TarIO.py
+++ b/src/PIL/TarIO.py
@@ -35,12 +35,14 @@ class TarIO(ContainerIO.ContainerIO):
s = self.fh.read(512)
if len(s) != 512:
- raise OSError("unexpected end of tar file")
+ msg = "unexpected end of tar file"
+ raise OSError(msg)
name = s[:100].decode("utf-8")
i = name.find("\0")
if i == 0:
- raise OSError("cannot find subfile")
+ msg = "cannot find subfile"
+ raise OSError(msg)
if i > 0:
name = name[:i]
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index cd454b755..53fe6ef5c 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -73,7 +73,8 @@ class TgaImageFile(ImageFile.ImageFile):
or self.size[1] <= 0
or depth not in (1, 8, 16, 24, 32)
):
- raise SyntaxError("not a TGA file")
+ msg = "not a TGA file"
+ raise SyntaxError(msg)
# image mode
if imagetype in (3, 11):
@@ -89,7 +90,8 @@ class TgaImageFile(ImageFile.ImageFile):
if depth == 32:
self.mode = "RGBA"
else:
- raise SyntaxError("unknown TGA mode")
+ msg = "unknown TGA mode"
+ raise SyntaxError(msg)
# orientation
orientation = flags & 0x30
@@ -99,7 +101,8 @@ class TgaImageFile(ImageFile.ImageFile):
elif orientation in [0, 0x10]:
orientation = -1
else:
- raise SyntaxError("unknown TGA orientation")
+ msg = "unknown TGA orientation"
+ raise SyntaxError(msg)
self.info["orientation"] = orientation
@@ -175,7 +178,8 @@ def _save(im, fp, filename):
try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as TGA") from e
+ msg = f"cannot write mode {im.mode} as TGA"
+ raise OSError(msg) from e
if "rle" in im.encoderinfo:
rle = im.encoderinfo["rle"]
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index 766d46ffb..431edfd9b 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -173,6 +173,7 @@ OPEN_INFO = {
(II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
(II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
+ (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
(II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
(MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
(II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
@@ -256,6 +257,8 @@ OPEN_INFO = {
(MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
}
+MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys())
+
PREFIXES = [
b"MM\x00\x2A", # Valid TIFF header with big-endian byte order
b"II\x2A\x00", # Valid TIFF header with little-endian byte order
@@ -497,14 +500,16 @@ class ImageFileDirectory_v2(MutableMapping):
:param prefix: Override the endianness of the file.
"""
if not _accept(ifh):
- raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)")
+ msg = f"not a TIFF file (header {repr(ifh)} not valid)"
+ raise SyntaxError(msg)
self._prefix = prefix if prefix is not None else ifh[:2]
if self._prefix == MM:
self._endian = ">"
elif self._prefix == II:
self._endian = "<"
else:
- raise SyntaxError("not a TIFF IFD")
+ msg = "not a TIFF IFD"
+ raise SyntaxError(msg)
self._bigtiff = ifh[2] == 43
self.group = group
self.tagtype = {}
@@ -521,7 +526,8 @@ class ImageFileDirectory_v2(MutableMapping):
@legacy_api.setter
def legacy_api(self, value):
- raise Exception("Not allowing setting of legacy api")
+ msg = "Not allowing setting of legacy api"
+ raise Exception(msg)
def reset(self):
self._tags_v1 = {} # will remain empty if legacy_api is false
@@ -716,6 +722,8 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(1) # Basic type, except for the legacy API.
def write_byte(self, data):
+ if isinstance(data, int):
+ data = bytes((data,))
return data
@_register_loader(2, 1)
@@ -727,6 +735,8 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_writer(2)
def write_string(self, value):
# remerge of https://github.com/python-pillow/Pillow/pull/1416
+ if isinstance(value, int):
+ value = str(value)
if not isinstance(value, bytes):
value = value.encode("ascii", "replace")
return value + b"\0"
@@ -773,10 +783,11 @@ class ImageFileDirectory_v2(MutableMapping):
def _ensure_read(self, fp, size):
ret = fp.read(size)
if len(ret) != size:
- raise OSError(
+ msg = (
"Corrupt EXIF data. "
f"Expecting to read {size} bytes but only got {len(ret)}. "
)
+ raise OSError(msg)
return ret
def load(self, fp):
@@ -903,7 +914,8 @@ class ImageFileDirectory_v2(MutableMapping):
if stripoffsets is not None:
tag, typ, count, value, data = entries[stripoffsets]
if data:
- raise NotImplementedError("multistrip support not yet implemented")
+ msg = "multistrip support not yet implemented"
+ raise NotImplementedError(msg)
value = self._pack("L", self._unpack("L", value)[0] + offset)
entries[stripoffsets] = tag, typ, count, value, data
@@ -1116,7 +1128,8 @@ class TiffImageFile(ImageFile.ImageFile):
while len(self._frame_pos) <= frame:
if not self.__next:
- raise EOFError("no more images in TIFF file")
+ msg = "no more images in TIFF file"
+ raise EOFError(msg)
logger.debug(
f"Seeking to frame {frame}, on frame {self.__frame}, "
f"__next {self.__next}, location: {self.fp.tell()}"
@@ -1148,39 +1161,6 @@ class TiffImageFile(ImageFile.ImageFile):
"""Return the current frame number"""
return self.__frame
- def get_child_images(self):
- if SUBIFD not in self.tag_v2:
- return []
- child_images = []
- exif = self.getexif()
- offset = None
- for im_offset in self.tag_v2[SUBIFD]:
- # reset buffered io handle in case fp
- # was passed to libtiff, invalidating the buffer
- current_offset = self._fp.tell()
- if offset is None:
- offset = current_offset
-
- fp = self._fp
- ifd = exif._get_ifd_dict(im_offset)
- jpegInterchangeFormat = ifd.get(513)
- if jpegInterchangeFormat is not None:
- fp.seek(jpegInterchangeFormat)
- jpeg_data = fp.read(ifd.get(514))
-
- fp = io.BytesIO(jpeg_data)
-
- with Image.open(fp) as im:
- if jpegInterchangeFormat is None:
- im._frame_pos = [im_offset]
- im._seek(0)
- im.load()
- child_images.append(im)
-
- if offset is not None:
- self._fp.seek(offset)
- return child_images
-
def getxmp(self):
"""
Returns a dictionary containing the XMP tags.
@@ -1256,7 +1236,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.load_prepare()
if not len(self.tile) == 1:
- raise OSError("Not exactly one tile")
+ msg = "Not exactly one tile"
+ raise OSError(msg)
# (self._compression, (extents tuple),
# 0, (rawmode, self._compression, fp))
@@ -1288,7 +1269,8 @@ class TiffImageFile(ImageFile.ImageFile):
try:
decoder.setimage(self.im, extents)
except ValueError as e:
- raise OSError("Couldn't set the image") from e
+ msg = "Couldn't set the image"
+ raise OSError(msg) from e
close_self_fp = self._exclusive_fp and not self.is_animated
if hasattr(self.fp, "getvalue"):
@@ -1342,7 +1324,8 @@ class TiffImageFile(ImageFile.ImageFile):
"""Setup this image object based on current tags"""
if 0xBC01 in self.tag_v2:
- raise OSError("Windows Media Photo files not yet supported")
+ msg = "Windows Media Photo files not yet supported"
+ raise OSError(msg)
# extract relevant tags
self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
@@ -1395,6 +1378,15 @@ class TiffImageFile(ImageFile.ImageFile):
SAMPLESPERPIXEL,
3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
)
+
+ if samples_per_pixel > MAX_SAMPLESPERPIXEL:
+ # DOS check, samples_per_pixel can be a Long, and we extend the tuple below
+ logger.error(
+ "More samples per pixel than can be decoded: %s", samples_per_pixel
+ )
+ msg = "Invalid value for samples per pixel"
+ raise SyntaxError(msg)
+
if samples_per_pixel < bps_actual_count:
# If a file has more values in bps_tuple than expected,
# remove the excess.
@@ -1405,7 +1397,8 @@ class TiffImageFile(ImageFile.ImageFile):
bps_tuple = bps_tuple * samples_per_pixel
if len(bps_tuple) != samples_per_pixel:
- raise SyntaxError("unknown data organization")
+ msg = "unknown data organization"
+ raise SyntaxError(msg)
# mode: check photometric interpretation and bits per pixel
key = (
@@ -1421,7 +1414,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.mode, rawmode = OPEN_INFO[key]
except KeyError as e:
logger.debug("- unsupported format")
- raise SyntaxError("unknown pixel mode") from e
+ msg = "unknown pixel mode"
+ raise SyntaxError(msg) from e
logger.debug(f"- raw mode: {rawmode}")
logger.debug(f"- pil mode: {self.mode}")
@@ -1537,7 +1531,8 @@ class TiffImageFile(ImageFile.ImageFile):
layer += 1
else:
logger.debug("- unsupported data organization")
- raise SyntaxError("unknown data organization")
+ msg = "unknown data organization"
+ raise SyntaxError(msg)
# Fix up info.
if ICCPROFILE in self.tag_v2:
@@ -1589,7 +1584,8 @@ def _save(im, fp, filename):
try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e:
- raise OSError(f"cannot write mode {im.mode} as TIFF") from e
+ msg = f"cannot write mode {im.mode} as TIFF"
+ raise OSError(msg) from e
ifd = ImageFileDirectory_v2(prefix=prefix)
@@ -1754,11 +1750,11 @@ def _save(im, fp, filename):
if "quality" in encoderinfo:
quality = encoderinfo["quality"]
if not isinstance(quality, int) or quality < 0 or quality > 100:
- raise ValueError("Invalid quality setting")
+ msg = "Invalid quality setting"
+ raise ValueError(msg)
if compression != "jpeg":
- raise ValueError(
- "quality setting only supported for 'jpeg' compression"
- )
+ msg = "quality setting only supported for 'jpeg' compression"
+ raise ValueError(msg)
ifd[JPEGQUALITY] = quality
logger.debug("Saving using libtiff encoder")
@@ -1855,7 +1851,8 @@ def _save(im, fp, filename):
if s:
break
if s < 0:
- raise OSError(f"encoder error {s} when writing image file")
+ msg = f"encoder error {s} when writing image file"
+ raise OSError(msg)
else:
for tag in blocklist:
@@ -1930,7 +1927,8 @@ class AppendingTiffWriter:
elif iimm == b"MM\x00\x2a":
self.setEndian(">")
else:
- raise RuntimeError("Invalid TIFF file header")
+ msg = "Invalid TIFF file header"
+ raise RuntimeError(msg)
self.skipIFDs()
self.goToEnd()
@@ -1944,12 +1942,14 @@ class AppendingTiffWriter:
iimm = self.f.read(4)
if not iimm:
- # raise RuntimeError("nothing written into new page")
+ # msg = "nothing written into new page"
+ # raise RuntimeError(msg)
# Make it easy to finish a frame without committing to a new one.
return
if iimm != self.IIMM:
- raise RuntimeError("IIMM of new page doesn't match IIMM of first page")
+ msg = "IIMM of new page doesn't match IIMM of first page"
+ raise RuntimeError(msg)
ifd_offset = self.readLong()
ifd_offset += self.offsetOfNewPage
@@ -2023,29 +2023,34 @@ class AppendingTiffWriter:
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
+ msg = f"wrote only {bytes_written} bytes but wanted 4"
+ raise RuntimeError(msg)
def rewriteLastShort(self, value):
self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")
+ msg = f"wrote only {bytes_written} bytes but wanted 2"
+ raise RuntimeError(msg)
def rewriteLastLong(self, value):
self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
+ msg = f"wrote only {bytes_written} bytes but wanted 4"
+ raise RuntimeError(msg)
def writeShort(self, value):
bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")
+ msg = f"wrote only {bytes_written} bytes but wanted 2"
+ raise RuntimeError(msg)
def writeLong(self, value):
bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4:
- raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")
+ msg = f"wrote only {bytes_written} bytes but wanted 4"
+ raise RuntimeError(msg)
def close(self):
self.finalize()
@@ -2088,7 +2093,8 @@ class AppendingTiffWriter:
def fixOffsets(self, count, isShort=False, isLong=False):
if not isShort and not isLong:
- raise RuntimeError("offset is neither short nor long")
+ msg = "offset is neither short nor long"
+ raise RuntimeError(msg)
for i in range(count):
offset = self.readShort() if isShort else self.readLong()
@@ -2096,7 +2102,8 @@ class AppendingTiffWriter:
if isShort and offset >= 65536:
# offset is now too large - we must convert shorts to longs
if count != 1:
- raise RuntimeError("not implemented") # XXX TODO
+ msg = "not implemented"
+ raise RuntimeError(msg) # XXX TODO
# simple case - the offset is just one and therefore it is
# local (not referenced with another offset)
diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py
index 3f3a1ccd2..9b5277138 100644
--- a/src/PIL/TiffTags.py
+++ b/src/PIL/TiffTags.py
@@ -232,7 +232,39 @@ TAGS_V2_GROUPS = {
41730: ("CFAPattern", UNDEFINED, 1),
},
# GPSInfoIFD
- 34853: {},
+ 34853: {
+ 0: ("GPSVersionID", BYTE, 4),
+ 1: ("GPSLatitudeRef", ASCII, 2),
+ 2: ("GPSLatitude", RATIONAL, 3),
+ 3: ("GPSLongitudeRef", ASCII, 2),
+ 4: ("GPSLongitude", RATIONAL, 3),
+ 5: ("GPSAltitudeRef", BYTE, 1),
+ 6: ("GPSAltitude", RATIONAL, 1),
+ 7: ("GPSTimeStamp", RATIONAL, 3),
+ 8: ("GPSSatellites", ASCII, 0),
+ 9: ("GPSStatus", ASCII, 2),
+ 10: ("GPSMeasureMode", ASCII, 2),
+ 11: ("GPSDOP", RATIONAL, 1),
+ 12: ("GPSSpeedRef", ASCII, 2),
+ 13: ("GPSSpeed", RATIONAL, 1),
+ 14: ("GPSTrackRef", ASCII, 2),
+ 15: ("GPSTrack", RATIONAL, 1),
+ 16: ("GPSImgDirectionRef", ASCII, 2),
+ 17: ("GPSImgDirection", RATIONAL, 1),
+ 18: ("GPSMapDatum", ASCII, 0),
+ 19: ("GPSDestLatitudeRef", ASCII, 2),
+ 20: ("GPSDestLatitude", RATIONAL, 3),
+ 21: ("GPSDestLongitudeRef", ASCII, 2),
+ 22: ("GPSDestLongitude", RATIONAL, 3),
+ 23: ("GPSDestBearingRef", ASCII, 2),
+ 24: ("GPSDestBearing", RATIONAL, 1),
+ 25: ("GPSDestDistanceRef", ASCII, 2),
+ 26: ("GPSDestDistance", RATIONAL, 1),
+ 27: ("GPSProcessingMethod", UNDEFINED, 0),
+ 28: ("GPSAreaInformation", UNDEFINED, 0),
+ 29: ("GPSDateStamp", ASCII, 11),
+ 30: ("GPSDifferential", SHORT, 1),
+ },
# InteroperabilityIFD
40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)},
}
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index c1f4b730f..1d074f78c 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -98,6 +98,15 @@ class WebPImageFile(ImageFile.ImageFile):
return None
return self.getexif()._get_merged_dict()
+ 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["xmp"]) if "xmp" in self.info else {}
+
def seek(self, frame):
if not self._seek_check(frame):
return
@@ -121,7 +130,8 @@ class WebPImageFile(ImageFile.ImageFile):
if ret is None:
self._reset() # Reset just to be safe
self.seek(0)
- raise EOFError("failed to decode next frame in WebP file")
+ msg = "failed to decode next frame in WebP file"
+ raise EOFError(msg)
# Compute duration
data, timestamp = ret
@@ -224,9 +234,8 @@ def _save_all(im, fp, filename):
or len(background) != 4
or not all(0 <= v < 256 for v in background)
):
- raise OSError(
- f"Background color is not an RGBA tuple clamped to (0-255): {background}"
- )
+ msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}"
+ raise OSError(msg)
# Convert to packed uint
bg_r, bg_g, bg_b, bg_a = background
@@ -302,7 +311,8 @@ def _save_all(im, fp, filename):
# Get the final output from the encoder
data = enc.assemble(icc_profile, exif, xmp)
if data is None:
- raise OSError("cannot write file as WebP (encoder returned None)")
+ msg = "cannot write file as WebP (encoder returned None)"
+ raise OSError(msg)
fp.write(data)
@@ -311,11 +321,14 @@ def _save(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile") or ""
- exif = im.encoderinfo.get("exif", "")
+ exif = im.encoderinfo.get("exif", b"")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
+ if exif.startswith(b"Exif\x00\x00"):
+ exif = exif[6:]
xmp = im.encoderinfo.get("xmp", "")
method = im.encoderinfo.get("method", 4)
+ exact = 1 if im.encoderinfo.get("exact") else 0
if im.mode not in _VALID_WEBP_LEGACY_MODES:
alpha = (
@@ -334,11 +347,13 @@ def _save(im, fp, filename):
im.mode,
icc_profile,
method,
+ exact,
exif,
xmp,
)
if data is None:
- raise OSError("cannot write file as WebP (encoder returned None)")
+ msg = "cannot write file as WebP (encoder returned None)"
+ raise OSError(msg)
fp.write(data)
diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py
index 2f54cdebb..639730b8e 100644
--- a/src/PIL/WmfImagePlugin.py
+++ b/src/PIL/WmfImagePlugin.py
@@ -109,7 +109,8 @@ class WmfStubImageFile(ImageFile.StubImageFile):
# sanity check (standard metafile header)
if s[22:26] != b"\x01\x00\t\x00":
- raise SyntaxError("Unsupported WMF file format")
+ msg = "Unsupported WMF file format"
+ raise SyntaxError(msg)
elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF":
# enhanced metafile
@@ -137,7 +138,8 @@ class WmfStubImageFile(ImageFile.StubImageFile):
self.info["dpi"] = xdpi, ydpi
else:
- raise SyntaxError("Unsupported file format")
+ msg = "Unsupported file format"
+ raise SyntaxError(msg)
self.mode = "RGB"
self._size = size
@@ -162,7 +164,8 @@ class WmfStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
- raise OSError("WMF save handler not installed")
+ msg = "WMF save handler not installed"
+ raise OSError(msg)
_handler.save(im, fp, filename)
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index 4efedb77e..f0e05e867 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -49,7 +49,8 @@ class XVThumbImageFile(ImageFile.ImageFile):
# check magic
if not _accept(self.fp.read(6)):
- raise SyntaxError("not an XV thumbnail file")
+ msg = "not an XV thumbnail file"
+ raise SyntaxError(msg)
# Skip to beginning of next line
self.fp.readline()
@@ -58,7 +59,8 @@ class XVThumbImageFile(ImageFile.ImageFile):
while True:
s = self.fp.readline()
if not s:
- raise SyntaxError("Unexpected EOF reading XV thumbnail file")
+ msg = "Unexpected EOF reading XV thumbnail file"
+ raise SyntaxError(msg)
if s[0] != 35: # ie. when not a comment: '#'
break
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index 59acabeba..ad18e0031 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -53,7 +53,8 @@ class XbmImageFile(ImageFile.ImageFile):
m = xbm_head.match(self.fp.read(512))
if not m:
- raise SyntaxError("not a XBM file")
+ msg = "not a XBM file"
+ raise SyntaxError(msg)
xsize = int(m.group("width"))
ysize = int(m.group("height"))
@@ -70,7 +71,8 @@ class XbmImageFile(ImageFile.ImageFile):
def _save(im, fp, filename):
if im.mode != "1":
- raise OSError(f"cannot write mode {im.mode} as XBM")
+ msg = f"cannot write mode {im.mode} as XBM"
+ raise OSError(msg)
fp.write(f"#define im_width {im.size[0]}\n".encode("ascii"))
fp.write(f"#define im_height {im.size[1]}\n".encode("ascii"))
diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py
index aaed2039d..5fae4cd68 100644
--- a/src/PIL/XpmImagePlugin.py
+++ b/src/PIL/XpmImagePlugin.py
@@ -40,13 +40,15 @@ class XpmImageFile(ImageFile.ImageFile):
def _open(self):
if not _accept(self.fp.read(9)):
- raise SyntaxError("not an XPM file")
+ msg = "not an XPM file"
+ raise SyntaxError(msg)
# skip forward to next string
while True:
s = self.fp.readline()
if not s:
- raise SyntaxError("broken XPM file")
+ msg = "broken XPM file"
+ raise SyntaxError(msg)
m = xpm_head.match(s)
if m:
break
@@ -57,7 +59,8 @@ class XpmImageFile(ImageFile.ImageFile):
bpp = int(m.group(4))
if pal > 256 or bpp != 1:
- raise ValueError("cannot read this XPM file")
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
#
# load palette description
@@ -91,13 +94,15 @@ class XpmImageFile(ImageFile.ImageFile):
)
else:
# unknown colour
- raise ValueError("cannot read this XPM file")
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
break
else:
# missing colour key
- raise ValueError("cannot read this XPM file")
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
self.mode = "P"
self.palette = ImagePalette.raw("RGB", b"".join(palette))
diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py
index 30a8a8971..7c4b1623d 100644
--- a/src/PIL/_deprecate.py
+++ b/src/PIL/_deprecate.py
@@ -43,14 +43,17 @@ def deprecate(
if when is None:
removed = "a future version"
elif when <= int(__version__.split(".")[0]):
- raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.")
+ msg = f"{deprecated} {is_} deprecated and should be removed."
+ raise RuntimeError(msg)
elif when == 10:
removed = "Pillow 10 (2023-07-01)"
else:
- raise ValueError(f"Unknown removal version, update {__name__}?")
+ msg = f"Unknown removal version, update {__name__}?"
+ raise ValueError(msg)
if replacement and action:
- raise ValueError("Use only one of 'replacement' and 'action'")
+ msg = "Use only one of 'replacement' and 'action'"
+ raise ValueError(msg)
if replacement:
action = f". Use {replacement} instead."
diff --git a/src/PIL/_version.py b/src/PIL/_version.py
index 8e736a432..1cc1d0f1c 100644
--- a/src/PIL/_version.py
+++ b/src/PIL/_version.py
@@ -1,2 +1,2 @@
# Master version for Pillow
-__version__ = "9.3.0.dev0"
+__version__ = "9.4.0.dev0"
diff --git a/src/PIL/features.py b/src/PIL/features.py
index 3838568f3..6f9d99e76 100644
--- a/src/PIL/features.py
+++ b/src/PIL/features.py
@@ -25,7 +25,8 @@ def check_module(feature):
:raises ValueError: If the module is not defined in this version of Pillow.
"""
if not (feature in modules):
- raise ValueError(f"Unknown module {feature}")
+ msg = f"Unknown module {feature}"
+ raise ValueError(msg)
module, ver = modules[feature]
@@ -78,7 +79,8 @@ def check_codec(feature):
:raises ValueError: If the codec is not defined in this version of Pillow.
"""
if feature not in codecs:
- raise ValueError(f"Unknown codec {feature}")
+ msg = f"Unknown codec {feature}"
+ raise ValueError(msg)
codec, lib = codecs[feature]
@@ -135,7 +137,8 @@ def check_feature(feature):
:raises ValueError: If the feature is not defined in this version of Pillow.
"""
if feature not in features:
- raise ValueError(f"Unknown feature {feature}")
+ msg = f"Unknown feature {feature}"
+ raise ValueError(msg)
module, flag, ver = features[feature]
diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c
index 5b3f18ace..16b9a2edd 100644
--- a/src/Tk/tkImaging.c
+++ b/src/Tk/tkImaging.c
@@ -364,17 +364,6 @@ load_tkinter_funcs(void) {
* tkinter dynamic library (module).
*/
-/* From module __file__ attribute to char *string for dlopen. */
-char *
-fname2char(PyObject *fname) {
- PyObject *bytes;
- bytes = PyUnicode_EncodeFSDefault(fname);
- if (bytes == NULL) {
- return NULL;
- }
- return PyBytes_AsString(bytes);
-}
-
#include
void *
@@ -442,7 +431,7 @@ load_tkinter_funcs(void) {
int ret = -1;
void *main_program, *tkinter_lib;
char *tkinter_libname;
- PyObject *pModule = NULL, *pString = NULL;
+ PyObject *pModule = NULL, *pString = NULL, *pBytes = NULL;
/* Try loading from the main program namespace first */
main_program = dlopen(NULL, RTLD_LAZY);
@@ -462,7 +451,12 @@ load_tkinter_funcs(void) {
if (pString == NULL) {
goto exit;
}
- tkinter_libname = fname2char(pString);
+ /* From module __file__ attribute to char *string for dlopen. */
+ pBytes = PyUnicode_EncodeFSDefault(pString);
+ if (pBytes == NULL) {
+ goto exit;
+ }
+ tkinter_libname = PyBytes_AsString(pBytes);
if (tkinter_libname == NULL) {
goto exit;
}
@@ -478,6 +472,7 @@ exit:
dlclose(main_program);
Py_XDECREF(pModule);
Py_XDECREF(pString);
+ Py_XDECREF(pBytes);
return ret;
}
#endif /* end not Windows */
diff --git a/src/_imaging.c b/src/_imaging.c
index 0888188fb..05e1370f6 100644
--- a/src/_imaging.c
+++ b/src/_imaging.c
@@ -1531,25 +1531,21 @@ if (PySequence_Check(op)) { \
PyErr_SetString(PyExc_TypeError, must_be_sequence);
return NULL;
}
+ int endian = strncmp(image->mode, "I;16", 4) == 0 ? (strcmp(image->mode, "I;16B") == 0 ? 2 : 1) : 0;
double value;
- if (scale == 1.0 && offset == 0.0) {
- /* Clipped data */
- for (i = x = y = 0; i < n; i++) {
- set_value_to_item(seq, i);
- image->image8[y][x] = (UINT8)CLIP8(value);
- if (++x >= (int)image->xsize) {
- x = 0, y++;
- }
+ for (i = x = y = 0; i < n; i++) {
+ set_value_to_item(seq, i);
+ if (scale != 1.0 || offset != 0.0) {
+ value = value * scale + offset;
}
-
- } else {
- /* Scaled and clipped data */
- for (i = x = y = 0; i < n; i++) {
- set_value_to_item(seq, i);
- image->image8[y][x] = CLIP8(value * scale + offset);
- if (++x >= (int)image->xsize) {
- x = 0, y++;
- }
+ if (endian == 0) {
+ image->image8[y][x] = (UINT8)CLIP8(value);
+ } else {
+ image->image8[y][x * 2 + (endian == 2 ? 1 : 0)] = CLIP8((int)value % 256);
+ image->image8[y][x * 2 + (endian == 2 ? 0 : 1)] = CLIP8((int)value >> 8);
+ }
+ if (++x >= (int)image->xsize) {
+ x = 0, y++;
}
}
PyErr_Clear(); /* Avoid weird exceptions */
@@ -1829,7 +1825,7 @@ _resize(ImagingObject *self, PyObject *args) {
box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) {
imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]);
} else if (filter == IMAGING_TRANSFORM_NEAREST) {
- double a[6];
+ double a[8];
memset(a, 0, sizeof a);
a[0] = (double)(box[2] - box[0]) / xsize;
diff --git a/src/_imagingft.c b/src/_imagingft.c
index 8f19b763c..b52d6353e 100644
--- a/src/_imagingft.c
+++ b/src/_imagingft.c
@@ -303,7 +303,7 @@ text_layout_raqm(
goto failed;
}
- len = PySequence_Size(seq);
+ len = PySequence_Fast_GET_SIZE(seq);
for (j = 0; j < len; j++) {
PyObject *item = PySequence_Fast_GET_ITEM(seq, j);
char *feature = NULL;
@@ -311,23 +311,26 @@ text_layout_raqm(
PyObject *bytes;
if (!PyUnicode_Check(item)) {
+ Py_DECREF(seq);
PyErr_SetString(PyExc_TypeError, "expected a string");
goto failed;
}
-
- if (PyUnicode_Check(item)) {
- bytes = PyUnicode_AsUTF8String(item);
- if (bytes == NULL) {
- goto failed;
- }
- feature = PyBytes_AS_STRING(bytes);
- size = PyBytes_GET_SIZE(bytes);
+ bytes = PyUnicode_AsUTF8String(item);
+ if (bytes == NULL) {
+ Py_DECREF(seq);
+ goto failed;
}
+ feature = PyBytes_AS_STRING(bytes);
+ size = PyBytes_GET_SIZE(bytes);
if (!raqm_add_font_feature(rq, feature, size)) {
+ Py_DECREF(seq);
+ Py_DECREF(bytes);
PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed");
goto failed;
}
+ Py_DECREF(bytes);
}
+ Py_DECREF(seq);
}
if (!raqm_set_freetype_face(rq, self->face)) {
@@ -774,13 +777,15 @@ font_render(FontObject *self, PyObject *args) {
const char *lang = NULL;
PyObject *features = Py_None;
PyObject *string;
+ float x_start = 0;
+ float y_start = 0;
/* render string into given buffer (the buffer *must* have
the right size, or this will crash) */
if (!PyArg_ParseTuple(
args,
- "On|zzOziL:render",
+ "On|zzOziLff:render",
&string,
&id,
&mode,
@@ -788,7 +793,9 @@ font_render(FontObject *self, PyObject *args) {
&features,
&lang,
&stroke_width,
- &foreground_ink_long)) {
+ &foreground_ink_long,
+ &x_start,
+ &y_start)) {
return NULL;
}
@@ -873,8 +880,8 @@ font_render(FontObject *self, PyObject *args) {
}
/* set pen position to text origin */
- x = (-x_min + stroke_width) << 6;
- y = (-y_max + (-stroke_width)) << 6;
+ x = (-x_min + stroke_width + x_start) * 64;
+ y = (-y_max + (-stroke_width) - y_start) * 64;
if (stroker == NULL) {
load_flags |= FT_LOAD_RENDER;
@@ -953,7 +960,7 @@ font_render(FontObject *self, PyObject *args) {
/* we didn't ask for color, fall through to default */
#endif
default:
- PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode");
+ PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
@@ -1020,7 +1027,7 @@ font_render(FontObject *self, PyObject *args) {
}
}
} else {
- PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode");
+ PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode");
goto glyph_error;
}
}
@@ -1179,7 +1186,7 @@ font_setvaraxes(FontObject *self, PyObject *args) {
}
num_coords = PyObject_Length(axes);
- coords = malloc(2 * sizeof(coords));
+ coords = (FT_Fixed*)malloc(num_coords * sizeof(FT_Fixed));
if (coords == NULL) {
return PyErr_NoMemory();
}
diff --git a/src/_webp.c b/src/_webp.c
index fd99116cb..493e0709c 100644
--- a/src/_webp.c
+++ b/src/_webp.c
@@ -178,12 +178,11 @@ _anim_encoder_new(PyObject *self, PyObject *args) {
return NULL;
}
-PyObject *
+void
_anim_encoder_dealloc(PyObject *self) {
WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self;
WebPPictureFree(&(encp->frame));
WebPAnimEncoderDelete(encp->enc);
- Py_RETURN_NONE;
}
PyObject *
@@ -400,12 +399,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) {
return NULL;
}
-PyObject *
+void
_anim_decoder_dealloc(PyObject *self) {
WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self;
WebPDataClear(&(decp->data));
WebPAnimDecoderDelete(decp->dec);
- Py_RETURN_NONE;
}
PyObject *
@@ -576,6 +574,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
int lossless;
float quality_factor;
int method;
+ int exact;
uint8_t *rgb;
uint8_t *icc_bytes;
uint8_t *exif_bytes;
@@ -597,7 +596,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "y#iiifss#is#s#",
+ "y#iiifss#iis#s#",
(char **)&rgb,
&size,
&width,
@@ -608,6 +607,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
&icc_bytes,
&icc_size,
&method,
+ &exact,
&exif_bytes,
&exif_size,
&xmp_bytes,
@@ -633,6 +633,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) {
config.lossless = lossless;
config.quality = quality_factor;
config.method = method;
+#if WEBP_ENCODER_ABI_VERSION >= 0x0209
+ // the "exact" flag is only available in libwebp 0.5.0 and later
+ config.exact = exact;
+#endif
// Validate the config
if (!WebPValidateConfig(&config)) {
diff --git a/src/decode.c b/src/decode.c
index cb018a4e7..7a9b956c5 100644
--- a/src/decode.c
+++ b/src/decode.c
@@ -376,11 +376,8 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) {
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";
+ actual = "RGB";
break;
default:
PyErr_SetString(PyExc_ValueError, "block compression type unknown");
diff --git a/src/encode.c b/src/encode.c
index 72c7f64d0..21c42d915 100644
--- a/src/encode.c
+++ b/src/encode.c
@@ -1048,6 +1048,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
PyObject *qtables = NULL;
unsigned int *qarrays = NULL;
int qtablesLen = 0;
+ char *comment = NULL;
+ Py_ssize_t comment_size;
char *extra = NULL;
Py_ssize_t extra_size;
char *rawExif = NULL;
@@ -1055,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(
args,
- "ss|nnnnnnnnOy#y#",
+ "ss|nnnnnnnnOz#y#y#",
&mode,
&rawmode,
&quality,
@@ -1067,6 +1069,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
&ydpi,
&subsampling,
&qtables,
+ &comment,
+ &comment_size,
&extra,
&extra_size,
&rawExif,
@@ -1090,13 +1094,28 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
return NULL;
}
- // Freed in JpegEncode, Case 5
+ // Freed in JpegEncode, Case 6
qarrays = get_qtables_arrays(qtables, &qtablesLen);
+ if (comment && comment_size > 0) {
+ /* malloc check ok, length is from python parsearg */
+ char *p = malloc(comment_size); // Freed in JpegEncode, Case 6
+ if (!p) {
+ return ImagingError_MemoryError();
+ }
+ memcpy(p, comment, comment_size);
+ comment = p;
+ } else {
+ comment = NULL;
+ }
+
if (extra && extra_size > 0) {
/* malloc check ok, length is from python parsearg */
- char *p = malloc(extra_size); // Freed in JpegEncode, Case 5
+ char *p = malloc(extra_size); // Freed in JpegEncode, Case 6
if (!p) {
+ if (comment) {
+ free(comment);
+ }
return ImagingError_MemoryError();
}
memcpy(p, extra, extra_size);
@@ -1107,8 +1126,11 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
if (rawExif && rawExifLen > 0) {
/* malloc check ok, length is from python parsearg */
- char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5
+ char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6
if (!pp) {
+ if (comment) {
+ free(comment);
+ }
if (extra) {
free(extra);
}
@@ -1134,6 +1156,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) {
((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype;
((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi;
((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi;
+ ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment;
+ ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size;
((JPEGENCODERSTATE *)encoder->state.context)->extra = extra;
((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size;
((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif;
@@ -1188,11 +1212,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
char *cinema_mode = "no";
OPJ_CINEMA_MODE cine_mode;
char mct = 0;
+ int sgnd = 0;
Py_ssize_t fd = -1;
if (!PyArg_ParseTuple(
args,
- "ss|OOOsOnOOOssbn",
+ "ss|OOOsOnOOOssbbn",
&mode,
&format,
&offset,
@@ -1207,6 +1232,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
&progression,
&cinema_mode,
&mct,
+ &sgnd,
&fd)) {
return NULL;
}
@@ -1305,6 +1331,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
context->progression = prog_order;
context->cinema_mode = cine_mode;
context->mct = mct;
+ context->sgnd = sgnd;
return (PyObject *)encoder;
}
diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c
index 514fb2929..83860c38a 100644
--- a/src/libImaging/Access.c
+++ b/src/libImaging/Access.c
@@ -43,23 +43,6 @@ add_item(const char *mode) {
return &access_table[i];
}
-/* fetch pointer to pixel line */
-
-static void *
-line_8(Imaging im, int x, int y) {
- return &im->image8[y][x];
-}
-
-static void *
-line_16(Imaging im, int x, int y) {
- return &im->image8[y][x + x];
-}
-
-static void *
-line_32(Imaging im, int x, int y) {
- return &im->image32[y][x];
-}
-
/* fetch individual pixel */
static void
@@ -187,36 +170,35 @@ put_pixel_32(Imaging im, int x, int y, const void *color) {
void
ImagingAccessInit() {
-#define ADD(mode_, line_, get_pixel_, put_pixel_) \
+#define ADD(mode_, get_pixel_, put_pixel_) \
{ \
ImagingAccess access = add_item(mode_); \
- access->line = line_; \
access->get_pixel = get_pixel_; \
access->put_pixel = put_pixel_; \
}
/* populate access table */
- ADD("1", line_8, get_pixel_8, put_pixel_8);
- ADD("L", line_8, get_pixel_8, put_pixel_8);
- ADD("LA", line_32, get_pixel, put_pixel);
- ADD("La", line_32, get_pixel, put_pixel);
- ADD("I", line_32, get_pixel_32, put_pixel_32);
- ADD("I;16", line_16, get_pixel_16L, put_pixel_16L);
- ADD("I;16L", line_16, get_pixel_16L, put_pixel_16L);
- ADD("I;16B", line_16, get_pixel_16B, put_pixel_16B);
- ADD("I;32L", line_32, get_pixel_32L, put_pixel_32L);
- ADD("I;32B", line_32, get_pixel_32B, put_pixel_32B);
- ADD("F", line_32, get_pixel_32, put_pixel_32);
- ADD("P", line_8, get_pixel_8, put_pixel_8);
- ADD("PA", line_32, get_pixel, put_pixel);
- ADD("RGB", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBA", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBa", line_32, get_pixel_32, put_pixel_32);
- ADD("RGBX", line_32, get_pixel_32, put_pixel_32);
- ADD("CMYK", line_32, get_pixel_32, put_pixel_32);
- ADD("YCbCr", line_32, get_pixel_32, put_pixel_32);
- ADD("LAB", line_32, get_pixel_32, put_pixel_32);
- ADD("HSV", line_32, get_pixel_32, put_pixel_32);
+ ADD("1", get_pixel_8, put_pixel_8);
+ ADD("L", get_pixel_8, put_pixel_8);
+ ADD("LA", get_pixel, put_pixel);
+ ADD("La", get_pixel, put_pixel);
+ ADD("I", get_pixel_32, put_pixel_32);
+ ADD("I;16", get_pixel_16L, put_pixel_16L);
+ ADD("I;16L", get_pixel_16L, put_pixel_16L);
+ ADD("I;16B", get_pixel_16B, put_pixel_16B);
+ ADD("I;32L", get_pixel_32L, put_pixel_32L);
+ ADD("I;32B", get_pixel_32B, put_pixel_32B);
+ ADD("F", get_pixel_32, put_pixel_32);
+ ADD("P", get_pixel_8, put_pixel_8);
+ ADD("PA", get_pixel, put_pixel);
+ ADD("RGB", get_pixel_32, put_pixel_32);
+ ADD("RGBA", get_pixel_32, put_pixel_32);
+ ADD("RGBa", get_pixel_32, put_pixel_32);
+ ADD("RGBX", get_pixel_32, put_pixel_32);
+ ADD("CMYK", get_pixel_32, put_pixel_32);
+ ADD("YCbCr", get_pixel_32, put_pixel_32);
+ ADD("LAB", get_pixel_32, put_pixel_32);
+ ADD("HSV", get_pixel_32, put_pixel_32);
}
ImagingAccess
diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c
index 22b36eb7a..a57b74b61 100644
--- a/src/libImaging/BcnDecode.c
+++ b/src/libImaging/BcnDecode.c
@@ -23,10 +23,6 @@ typedef struct {
UINT8 l;
} lum;
-typedef struct {
- FLOAT32 r, g, b;
-} rgb32f;
-
typedef struct {
UINT16 c0, c1;
UINT32 lut;
@@ -536,53 +532,53 @@ static const bc6_mode_info bc6_modes[] = {
/* Table.F, encoded as a sequence of bit indices */
static const UINT8 bc6_bit_packings[][75] = {
- {116, 132, 176, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17,
+ {116, 132, 180, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65,
- 66, 67, 68, 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128,
- 129, 130, 131, 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
- {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 172, 173, 132, 16, 17,
- 18, 19, 20, 21, 22, 133, 174, 116, 32, 33, 34, 35, 36, 37, 38,
- 175, 177, 176, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65,
+ 66, 67, 68, 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128,
+ 129, 130, 131, 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
+ {117, 164, 165, 0, 1, 2, 3, 4, 5, 6, 176, 177, 132, 16, 17,
+ 18, 19, 20, 21, 22, 133, 178, 116, 32, 33, 34, 35, 36, 37, 38,
+ 179, 181, 180, 48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65,
66, 67, 68, 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128,
129, 130, 131, 96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
48, 49, 50, 51, 52, 10, 112, 113, 114, 115, 64, 65, 66, 67, 26,
- 172, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, 128, 129, 130, 131,
- 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
+ 176, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131,
+ 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
48, 49, 50, 51, 10, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
- 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 173, 128, 129, 130, 131,
- 96, 97, 98, 99, 172, 174, 144, 145, 146, 147, 116, 175},
+ 26, 160, 161, 162, 163, 80, 81, 82, 83, 42, 177, 128, 129, 130, 131,
+ 96, 97, 98, 99, 176, 178, 144, 145, 146, 147, 116, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
48, 49, 50, 51, 10, 132, 112, 113, 114, 115, 64, 65, 66, 67, 26,
- 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131,
- 96, 97, 98, 99, 173, 174, 144, 145, 146, 147, 176, 175},
+ 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 42, 128, 129, 130, 131,
+ 96, 97, 98, 99, 177, 178, 144, 145, 146, 147, 180, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 132, 16, 17, 18, 19, 20,
- 21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 176,
+ 21, 22, 23, 24, 116, 32, 33, 34, 35, 36, 37, 38, 39, 40, 180,
48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
- 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131,
- 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
+ 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131,
+ 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
{0, 1, 2, 3, 4, 5, 6, 7, 164, 132, 16, 17, 18, 19, 20,
- 21, 22, 23, 174, 116, 32, 33, 34, 35, 36, 37, 38, 39, 175, 176,
+ 21, 22, 23, 178, 116, 32, 33, 34, 35, 36, 37, 38, 39, 179, 180,
48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68,
- 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131,
+ 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131,
96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149},
- {0, 1, 2, 3, 4, 5, 6, 7, 172, 132, 16, 17, 18, 19, 20,
- 21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 176,
+ {0, 1, 2, 3, 4, 5, 6, 7, 176, 132, 16, 17, 18, 19, 20,
+ 21, 22, 23, 117, 116, 32, 33, 34, 35, 36, 37, 38, 39, 165, 180,
48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
- 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 173, 128, 129, 130, 131,
- 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
- {0, 1, 2, 3, 4, 5, 6, 7, 173, 132, 16, 17, 18, 19, 20,
- 21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 177, 176,
+ 69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 177, 128, 129, 130, 131,
+ 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
+ {0, 1, 2, 3, 4, 5, 6, 7, 177, 132, 16, 17, 18, 19, 20,
+ 21, 22, 23, 133, 116, 32, 33, 34, 35, 36, 37, 38, 39, 181, 180,
48, 49, 50, 51, 52, 164, 112, 113, 114, 115, 64, 65, 66, 67, 68,
- 172, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131,
- 96, 97, 98, 99, 100, 174, 144, 145, 146, 147, 148, 175},
- {0, 1, 2, 3, 4, 5, 164, 172, 173, 132, 16, 17, 18, 19, 20,
- 21, 117, 133, 174, 116, 32, 33, 34, 35, 36, 37, 165, 175, 177, 176,
+ 176, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131,
+ 96, 97, 98, 99, 100, 178, 144, 145, 146, 147, 148, 179},
+ {0, 1, 2, 3, 4, 5, 164, 176, 177, 132, 16, 17, 18, 19, 20,
+ 21, 117, 133, 178, 116, 32, 33, 34, 35, 36, 37, 165, 179, 181, 180,
48, 49, 50, 51, 52, 53, 112, 113, 114, 115, 64, 65, 66, 67, 68,
69, 160, 161, 162, 163, 80, 81, 82, 83, 84, 85, 128, 129, 130, 131,
96, 97, 98, 99, 100, 101, 144, 145, 146, 147, 148, 149},
@@ -681,20 +677,31 @@ bc6_finalize(int v, int sign) {
}
}
+static UINT8
+bc6_clamp(float value) {
+ if (value < 0.0f) {
+ return 0;
+ } else if (value > 1.0f) {
+ return 255;
+ } else {
+ return (UINT8) (value * 255.0f);
+ }
+}
+
static void
-bc6_lerp(rgb32f *col, int *e0, int *e1, int s, int sign) {
+bc6_lerp(rgba *col, int *e0, int *e1, int s, int sign) {
int r, g, b;
int t = 64 - s;
r = (e0[0] * t + e1[0] * s) >> 6;
g = (e0[1] * t + e1[1] * s) >> 6;
b = (e0[2] * t + e1[2] * s) >> 6;
- col->r = bc6_finalize(r, sign);
- col->g = bc6_finalize(g, sign);
- col->b = bc6_finalize(b, sign);
+ col->r = bc6_clamp(bc6_finalize(r, sign));
+ col->g = bc6_clamp(bc6_finalize(g, sign));
+ col->b = bc6_clamp(bc6_finalize(b, sign));
}
static void
-decode_bc6_block(rgb32f *col, const UINT8 *src, int sign) {
+decode_bc6_block(rgba *col, const UINT8 *src, int sign) {
UINT16 endpoints[12]; /* storage for r0, g0, b0, r1, ... */
int ueps[12];
int i, i0, ib2, di, dw, mask, numep, s;
@@ -744,21 +751,16 @@ decode_bc6_block(rgb32f *col, const UINT8 *src, int sign) {
}
if (sign || info->tr) { /* sign-extend e1,2,3 if signed or deltas */
for (i = 3; i < numep; i += 3) {
- bc6_sign_extend(&endpoints[i + 0], info->rb);
+ bc6_sign_extend(&endpoints[i], info->rb);
bc6_sign_extend(&endpoints[i + 1], info->gb);
bc6_sign_extend(&endpoints[i + 2], info->bb);
}
}
if (info->tr) { /* apply deltas */
- for (i = 3; i < numep; i++) {
+ for (i = 3; i < numep; i += 3) {
endpoints[i] = (endpoints[i] + endpoints[0]) & mask;
- }
- if (sign) {
- for (i = 3; i < numep; i += 3) {
- bc6_sign_extend(&endpoints[i + 0], info->rb);
- bc6_sign_extend(&endpoints[i + 1], info->gb);
- bc6_sign_extend(&endpoints[i + 2], info->bb);
- }
+ endpoints[i + 1] = (endpoints[i + 1] + endpoints[1]) & mask;
+ endpoints[i + 2] = (endpoints[i + 2] + endpoints[2]) & mask;
}
}
for (i = 0; i < numep; i++) {
@@ -862,8 +864,8 @@ decode_bcn(
break;
case 6:
while (bytes >= 16) {
- rgb32f col[16];
- decode_bc6_block(col, ptr, (state->state >> 4) & 1);
+ rgba col[16];
+ decode_bc6_block(col, ptr, strcmp(pixel_format, "BC6HS") == 0 ? 1 : 0);
put_block(im, state, (const char *)col, sizeof(col[0]), C);
ptr += 16;
bytes -= 16;
diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c
index fd6e268b5..aee7cda06 100644
--- a/src/libImaging/ColorLUT.c
+++ b/src/libImaging/ColorLUT.c
@@ -7,8 +7,8 @@
#define PRECISION_BITS (16 - 8 - 2)
#define PRECISION_ROUNDING (1 << (PRECISION_BITS - 1))
-/* 8 — scales are multiplied on byte.
- 6 — max index in the table
+/* 8 - scales are multiplied on byte.
+ 6 - max index in the table
(max size is 65, but index 64 is not reachable) */
#define SCALE_BITS (32 - 8 - 6)
#define SCALE_MASK ((1 << SCALE_BITS) - 1)
@@ -44,14 +44,14 @@ table_index3D(int index1D, int index2D, int index3D, int size1D, int size1D_2D)
Transforms colors of imIn using provided 3D lookup table
and puts the result in imOut. Returns imOut on success or 0 on error.
- imOut, imIn — images, should be the same size and may be the same image.
+ imOut, imIn - images, should be the same size and may be the same image.
Should have 3 or 4 channels.
- table_channels — number of channels in the lookup table, 3 or 4.
+ table_channels - number of channels in the lookup table, 3 or 4.
Should be less or equal than number of channels in imOut image;
- size1D, size_2D and size3D — dimensions of provided table;
- table — flat table,
- array with table_channels × size1D × size2D × size3D elements,
- where channels are changed first, then 1D, then 2D, then 3D.
+ size1D, size_2D and size3D - dimensions of provided table;
+ table - flat table,
+ array with table_channels * size1D * size2D * size3D elements,
+ where channels are changed first, then 1D, then 2D, then 3D.
Each element is signed 16-bit int where 0 is lowest output value
and 255 << PRECISION_BITS (16320) is highest value.
*/
diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c
index bdc680be4..b03bd02af 100644
--- a/src/libImaging/Convert.c
+++ b/src/libImaging/Convert.c
@@ -43,13 +43,6 @@
#define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114)
#define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000)
-#ifndef round
-double
-round(double x) {
- return floor(x + 0.5);
-}
-#endif
-
/* ------------------- */
/* 1 (bit) conversions */
/* ------------------- */
@@ -486,6 +479,25 @@ rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) {
}
}
+static void
+rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) {
+ int x;
+ unsigned int alpha;
+ for (x = 0; x < xsize; x++, in += 4) {
+ alpha = in[3];
+ if (alpha == 255 || alpha == 0) {
+ *out++ = in[0];
+ *out++ = in[1];
+ *out++ = in[2];
+ } else {
+ *out++ = CLIP8((255 * in[0]) / alpha);
+ *out++ = CLIP8((255 * in[1]) / alpha);
+ *out++ = CLIP8((255 * in[2]) / alpha);
+ }
+ *out++ = 255;
+ }
+}
+
/*
* Conversion of RGB + single transparent color to RGBA,
* where any pixel that matches the color will have the
@@ -941,6 +953,7 @@ static struct {
{"RGBA", "HSV", rgb2hsv},
{"RGBa", "RGBA", rgba2rgbA},
+ {"RGBa", "RGB", rgba2rgb_},
{"RGBX", "1", rgb2bit},
{"RGBX", "L", rgb2l},
diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h
index b65f8eadd..d9ded1852 100644
--- a/src/libImaging/Imaging.h
+++ b/src/libImaging/Imaging.h
@@ -124,7 +124,6 @@ struct ImagingMemoryInstance {
struct ImagingAccessInstance {
const char *mode;
- void *(*line)(Imaging im, int x, int y);
void (*get_pixel)(Imaging im, int x, int y, void *pixel);
void (*put_pixel)(Imaging im, int x, int y, const void *pixel);
};
diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h
index a876d3bb6..1d7550818 100644
--- a/src/libImaging/Jpeg.h
+++ b/src/libImaging/Jpeg.h
@@ -92,6 +92,10 @@ typedef struct {
/* in factors of DCTSIZE2 */
int qtablesLen;
+ /* Comment */
+ char *comment;
+ size_t comment_size;
+
/* Extra data (to be injected after header) */
char *extra;
int extra_size;
diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h
index d030b0c43..b28a0440a 100644
--- a/src/libImaging/Jpeg2K.h
+++ b/src/libImaging/Jpeg2K.h
@@ -85,6 +85,9 @@ typedef struct {
/* Set multiple component transformation */
char mct;
+ /* Signed */
+ int sgnd;
+
/* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */
OPJ_PROG_ORDER progression;
diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c
index fe5511ba5..db1c5c0c9 100644
--- a/src/libImaging/Jpeg2KEncode.c
+++ b/src/libImaging/Jpeg2KEncode.c
@@ -343,7 +343,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) {
image_params[n].x0 = image_params[n].y0 = 0;
image_params[n].prec = prec;
image_params[n].bpp = bpp;
- image_params[n].sgnd = 0;
+ image_params[n].sgnd = context->sgnd == 0 ? 0 : 1;
}
image = opj_image_create(components, image_params, color_space);
diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c
index a44debcaf..2a24eff39 100644
--- a/src/libImaging/JpegEncode.c
+++ b/src/libImaging/JpegEncode.c
@@ -277,6 +277,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
}
case 4:
+
+ if (context->comment) {
+ jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size);
+ }
+ state->state++;
+
+ case 5:
if (1024 > context->destination.pub.free_in_buffer) {
break;
}
@@ -301,7 +308,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
state->state++;
/* fall through */
- case 5:
+ case 6:
/* Finish compression */
if (context->destination.pub.free_in_buffer < 100) {
@@ -310,6 +317,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) {
jpeg_finish_compress(&context->cinfo);
/* Clean up */
+ if (context->comment) {
+ free(context->comment);
+ context->comment = NULL;
+ }
if (context->extra) {
free(context->extra);
context->extra = NULL;
diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c
index 137ed242a..182eb62a7 100644
--- a/src/libImaging/Matrix.c
+++ b/src/libImaging/Matrix.c
@@ -21,6 +21,7 @@ Imaging
ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
Imaging imOut;
int x, y;
+ ImagingSectionCookie cookie;
/* Assume there's enough data in the buffer */
if (!im) {
@@ -33,6 +34,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
return NULL;
}
+ ImagingSectionEnter(&cookie);
for (y = 0; y < im->ysize; y++) {
UINT8 *in = (UINT8 *)im->image[y];
UINT8 *out = (UINT8 *)imOut->image[y];
@@ -43,6 +45,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
in += 4;
}
}
+ ImagingSectionLeave(&cookie);
} else if (strlen(mode) == 3 && im->bands == 3) {
imOut = ImagingNewDirty(mode, im->xsize, im->ysize);
@@ -54,6 +57,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
UINT8 *in = (UINT8 *)im->image[y];
UINT8 *out = (UINT8 *)imOut->image[y];
+ ImagingSectionEnter(&cookie);
for (x = 0; x < im->xsize; x++) {
float v0 = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5;
float v1 = m[4] * in[0] + m[5] * in[1] + m[6] * in[2] + m[7] + 0.5;
@@ -64,6 +68,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
in += 4;
out += 4;
}
+ ImagingSectionLeave(&cookie);
}
} else {
return (Imaging)ImagingError_ModeError();
diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c
index fafd8141e..acf5202e5 100644
--- a/src/libImaging/Paste.c
+++ b/src/libImaging/Paste.c
@@ -432,18 +432,18 @@ fill_mask_L(
}
} else {
+ int alpha_channel = strcmp(imOut->mode, "RGBa") == 0 ||
+ strcmp(imOut->mode, "RGBA") == 0 ||
+ strcmp(imOut->mode, "La") == 0 ||
+ strcmp(imOut->mode, "LA") == 0 ||
+ strcmp(imOut->mode, "PA") == 0;
for (y = 0; y < ysize; y++) {
UINT8 *out = (UINT8 *)imOut->image[y + dy] + dx * pixelsize;
UINT8 *mask = (UINT8 *)imMask->image[y + sy] + sx;
for (x = 0; x < xsize; x++) {
for (i = 0; i < pixelsize; i++) {
UINT8 channel_mask = *mask;
- if ((strcmp(imOut->mode, "RGBa") == 0 ||
- strcmp(imOut->mode, "RGBA") == 0 ||
- strcmp(imOut->mode, "La") == 0 ||
- strcmp(imOut->mode, "LA") == 0 ||
- strcmp(imOut->mode, "PA") == 0) &&
- i != 3 && channel_mask != 0) {
+ if (alpha_channel && 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 dfa6d842d..783852c24 100644
--- a/src/libImaging/Quant.c
+++ b/src/libImaging/Quant.c
@@ -1717,7 +1717,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) {
withAlpha = !strcmp(im->mode, "RGBA");
int transparency = 0;
- unsigned char r, g, b;
+ unsigned char r = 0, g = 0, b = 0;
for (i = y = 0; y < im->ysize; y++) {
for (x = 0; x < im->xsize; x++, i++) {
p[i].v = im->image32[y][x];
diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c
index 7663f96a9..428cd93d2 100644
--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -771,11 +771,11 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) {
TRACE(("Opening using fd: %d for writing \n", clientstate->fp));
clientstate->tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode);
} else {
- // malloc a buffer to write the tif, we're going to need to realloc or something
+ // calloc a buffer to write the tif, we're going to need to realloc or something
// if we need bigger.
TRACE(("Opening a buffer for writing \n"));
- /* malloc check ok, small constant allocation */
- clientstate->data = malloc(bufsize);
+ /* calloc check ok, small constant allocation */
+ clientstate->data = calloc(bufsize, 1);
clientstate->size = bufsize;
clientstate->flrealloc = 1;
diff --git a/tox.ini b/tox.ini
index 21b5d4b50..9a41ca96b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,15 +1,13 @@
-# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it,
-# "python3 -m pip install tox" and then run "tox" from this directory.
-
[tox]
envlist =
lint
- py{37,38,39,310,311,py3}
+ py{py3, 311, 310, 39, 38, 37}
minversion = 1.9
[testenv]
+deps =
+ cffi
+ numpy
extras =
tests
commands =
@@ -17,16 +15,15 @@ commands =
{envpython} -m pip install --global-option="build_ext" --global-option="--inplace" .
{envpython} selftest.py
{envpython} -m pytest -W always {posargs}
-deps =
- cffi
- numpy
+allowlist_externals = make
[testenv:lint]
+passenv =
+ PRE_COMMIT_COLOR
+skip_install = true
+deps =
+ check-manifest
+ pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
check-manifest
-deps =
- pre-commit
- check-manifest
-skip_install = true
-passenv = PRE_COMMIT_COLOR
diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py
index f4515468f..f5050946c 100644
--- a/winbuild/build_prepare.py
+++ b/winbuild/build_prepare.py
@@ -1,5 +1,6 @@
import os
import platform
+import re
import shutil
import struct
import subprocess
@@ -111,6 +112,11 @@ deps = {
+ "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download",
"filename": "libjpeg-turbo-2.1.4.tar.gz",
"dir": "libjpeg-turbo-2.1.4",
+ "license": ["README.ijg", "LICENSE.md"],
+ "license_pattern": (
+ "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n=========="
+ ".+(libjpeg-turbo Licenses\n======================\n\n.+)$"
+ ),
"build": [
cmd_cmake(
[
@@ -132,9 +138,11 @@ deps = {
"bins": ["cjpeg.exe", "djpeg.exe"],
},
"zlib": {
- "url": "https://zlib.net/zlib1212.zip",
- "filename": "zlib1212.zip",
- "dir": "zlib-1.2.12",
+ "url": "https://zlib.net/zlib1213.zip",
+ "filename": "zlib1213.zip",
+ "dir": "zlib-1.2.13",
+ "license": "README",
+ "license_pattern": "Copyright notice:\n\n(.+)$",
"build": [
cmd_nmake(r"win32\Makefile.msc", "clean"),
cmd_nmake(r"win32\Makefile.msc", "zlib.lib"),
@@ -143,10 +151,69 @@ deps = {
"headers": [r"z*.h"],
"libs": [r"*.lib"],
},
+ "xz": {
+ "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download",
+ "filename": "xz-5.4.0.tar.gz",
+ "dir": "xz-5.4.0",
+ "license": "COPYING",
+ "patch": {
+ r"src\liblzma\api\lzma.h": {
+ "#ifndef LZMA_API_IMPORT": "#ifndef LZMA_API_IMPORT\n#define LZMA_API_STATIC", # noqa: E501
+ },
+ r"windows\vs2019\liblzma.vcxproj": {
+ # retarget to default toolset (selected by vcvarsall.bat)
+ "v142": "$(DefaultPlatformToolset)", # noqa: E501
+ # retarget to latest (selected by vcvarsall.bat)
+ "10.0": "$(WindowsSDKVersion)", # noqa: E501
+ },
+ },
+ "build": [
+ cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Clean"),
+ cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Build"),
+ cmd_mkdir(r"{inc_dir}\lzma"),
+ cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"),
+ ],
+ "headers": [r"src\liblzma\api\lzma.h"],
+ "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"],
+ },
+ "libwebp": {
+ "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz",
+ "filename": "libwebp-1.2.4.tar.gz",
+ "dir": "libwebp-1.2.4",
+ "license": "COPYING",
+ "build": [
+ cmd_rmdir(r"output\release-static"), # clean
+ cmd_nmake(
+ "Makefile.vc",
+ "all",
+ [
+ "CFG=release-static",
+ "RTLIBCFG=dynamic",
+ "OBJDIR=output",
+ "ARCH={architecture}",
+ "LIBWEBP_BASENAME=webp",
+ ],
+ ),
+ cmd_mkdir(r"{inc_dir}\webp"),
+ cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"),
+ ],
+ "libs": [r"output\release-static\{architecture}\lib\*.lib"],
+ },
"libtiff": {
- "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz",
- "filename": "tiff-4.4.0.tar.gz",
- "dir": "tiff-4.4.0",
+ "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz",
+ "filename": "tiff-4.5.0.tar.gz",
+ "dir": "tiff-4.5.0",
+ "license": "LICENSE.md",
+ "patch": {
+ r"libtiff\tif_lzma.c": {
+ # link against liblzma.lib
+ "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501
+ },
+ r"libtiff\tif_webp.c": {
+ # link against webp.lib
+ "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501
+ },
+ },
"build": [
cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"),
cmd_nmake(target="clean"),
@@ -156,26 +223,11 @@ deps = {
"libs": [r"libtiff\*.lib"],
# "bins": [r"libtiff\*.dll"],
},
- "libwebp": {
- "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz",
- "filename": "libwebp-1.2.4.tar.gz",
- "dir": "libwebp-1.2.4",
- "build": [
- cmd_rmdir(r"output\release-static"), # clean
- cmd_nmake(
- "Makefile.vc",
- "all",
- ["CFG=release-static", "OBJDIR=output", "ARCH={architecture}"],
- ),
- cmd_mkdir(r"{inc_dir}\webp"),
- cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"),
- ],
- "libs": [r"output\release-static\{architecture}\lib\*.lib"],
- },
"libpng": {
- "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.37/lpng1637.zip/download",
- "filename": "lpng1637.zip",
- "dir": "lpng1637",
+ "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download",
+ "filename": "lpng1639.zip",
+ "dir": "lpng1639",
+ "license": "LICENSE",
"build": [
# lint: do not inline
cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")),
@@ -186,10 +238,25 @@ deps = {
"headers": [r"png*.h"],
"libs": [r"libpng16.lib"],
},
+ "brotli": {
+ "url": "https://github.com/google/brotli/archive/refs/tags/v1.0.9.tar.gz",
+ "filename": "brotli-1.0.9.tar.gz",
+ "dir": "brotli-1.0.9",
+ "license": "LICENSE",
+ "build": [
+ cmd_cmake(),
+ cmd_nmake(target="clean"),
+ cmd_nmake(target="brotlicommon-static"),
+ cmd_nmake(target="brotlidec-static"),
+ cmd_xcopy(r"c\include", "{inc_dir}"),
+ ],
+ "libs": ["*.lib"],
+ },
"freetype": {
"url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501
"filename": "freetype-2.12.1.tar.gz",
"dir": "freetype-2.12.1",
+ "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"],
"patch": {
r"builds\windows\vc2010\freetype.vcxproj": {
# freetype setting is /MD for .dll and /MT for .lib, we need /MD
@@ -198,13 +265,13 @@ deps = {
'': '\n $(WindowsSDKVersion)', # noqa: E501
},
r"builds\windows\vc2010\freetype.user.props": {
- "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ", # noqa: E501
+ "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501
"": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501
"": "{lib_dir}", # noqa: E501
- "": "zlib.lib;libpng16.lib", # noqa: E501
+ "": "zlib.lib;libpng16.lib;brotlicommon-static.lib;brotlidec-static.lib", # noqa: E501
},
r"src/autofit/afshaper.c": {
- # link against harfbuzz.lib once it becomes available
+ # link against harfbuzz.lib
"#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501
},
},
@@ -222,9 +289,10 @@ deps = {
# "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
},
"lcms2": {
- "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.13.1.tar.gz/download",
- "filename": "lcms2-2.13.1.tar.gz",
- "dir": "lcms2-2.13.1",
+ "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download",
+ "filename": "lcms2-2.14.tar.gz",
+ "dir": "lcms2-2.14",
+ "license": "COPYING",
"patch": {
r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": {
# default is /MD for x86 and /MT for x64, we need /MD always
@@ -250,8 +318,14 @@ deps = {
"url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz",
"filename": "openjpeg-2.5.0.tar.gz",
"dir": "openjpeg-2.5.0",
+ "license": "LICENSE",
+ "patch": {
+ r"src\lib\openjp2\ht_dec.c": {
+ "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501
+ }
+ },
"build": [
- cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")),
+ cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")),
cmd_nmake(target="clean"),
cmd_nmake(target="openjp2"),
cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"),
@@ -264,6 +338,7 @@ deps = {
"url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501
"filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip",
"dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab",
+ "license": "COPYRIGHT",
"patch": {
"CMakeLists.txt": {
"if(OPENMP_FOUND)": "if(false)",
@@ -281,10 +356,12 @@ deps = {
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
- "url": "https://github.com/harfbuzz/harfbuzz/archive/5.2.0.zip",
- "filename": "harfbuzz-5.2.0.zip",
- "dir": "harfbuzz-5.2.0",
+ "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip",
+ "filename": "harfbuzz-6.0.0.zip",
+ "dir": "harfbuzz-6.0.0",
+ "license": "COPYING",
"build": [
+ cmd_set("CXXFLAGS", "-d2FH4-"),
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"),
cmd_nmake(target="harfbuzz"),
@@ -296,7 +373,9 @@ deps = {
"url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip",
"filename": "fribidi-1.0.12.zip",
"dir": "fribidi-1.0.12",
+ "license": "COPYING",
"build": [
+ cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"),
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
cmd_cmake(),
cmd_nmake(target="clean"),
@@ -392,22 +471,36 @@ def extract_dep(url, filename):
raise RuntimeError(ex)
print("Extracting " + filename)
+ sources_dir_abs = os.path.abspath(sources_dir)
if filename.endswith(".zip"):
with zipfile.ZipFile(file) as zf:
+ for member in zf.namelist():
+ member_abspath = os.path.abspath(os.path.join(sources_dir, member))
+ member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
+ if sources_dir_abs != member_prefix:
+ msg = "Attempted Path Traversal in Zip File"
+ raise RuntimeError(msg)
zf.extractall(sources_dir)
elif filename.endswith(".tar.gz") or filename.endswith(".tgz"):
with tarfile.open(file, "r:gz") as tgz:
+ for member in tgz.getnames():
+ member_abspath = os.path.abspath(os.path.join(sources_dir, member))
+ member_prefix = os.path.commonpath([sources_dir_abs, member_abspath])
+ if sources_dir_abs != member_prefix:
+ msg = "Attempted Path Traversal in Tar File"
+ raise RuntimeError(msg)
tgz.extractall(sources_dir)
else:
- raise RuntimeError("Unknown archive type: " + filename)
+ msg = "Unknown archive type: " + filename
+ raise RuntimeError(msg)
def write_script(name, lines):
name = os.path.join(build_dir, name)
lines = [line.format(**prefs) for line in lines]
print("Writing " + name)
- with open(name, "w") as f:
- f.write("\n\r".join(lines))
+ with open(name, "w", newline="") as f:
+ f.write(os.linesep.join(lines))
if verbose:
for line in lines:
print(" " + line)
@@ -431,6 +524,21 @@ def build_dep(name):
extract_dep(dep["url"], dep["filename"])
+ licenses = dep["license"]
+ if isinstance(licenses, str):
+ licenses = [licenses]
+ license_text = ""
+ for license_file in licenses:
+ with open(os.path.join(sources_dir, dir, license_file)) as f:
+ license_text += f.read()
+ if "license_pattern" in dep:
+ match = re.search(dep["license_pattern"], license_text, re.DOTALL)
+ license_text = "\n".join(match.groups())
+ assert len(license_text) > 50
+ with open(os.path.join(license_dir, f"{dir}.txt"), "w") as f:
+ print(f"Writing license {dir}.txt")
+ f.write(license_text)
+
for patch_file, patch_list in dep.get("patch", {}).items():
patch_file = os.path.join(sources_dir, dir, patch_file.format(**prefs))
with open(patch_file) as f:
@@ -477,6 +585,7 @@ def build_pillow():
cmd_cd("{pillow_dir}"),
*prefs["header"],
cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow
+ cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT
r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501
]
@@ -520,7 +629,8 @@ if __name__ == "__main__":
elif arg == "--srcdir":
sources_dir = os.path.sep + "src"
else:
- raise ValueError("Unknown parameter: " + arg)
+ msg = "Unknown parameter: " + arg
+ raise ValueError(msg)
# dependency cache directory
os.makedirs(depends_dir, exist_ok=True)
@@ -536,9 +646,8 @@ if __name__ == "__main__":
msvs = find_msvs()
if msvs is None:
- raise RuntimeError(
- "Visual Studio not found. Please install Visual Studio 2017 or newer."
- )
+ msg = "Visual Studio not found. Please install Visual Studio 2017 or newer."
+ raise RuntimeError(msg)
print("Found Visual Studio at:", msvs["vs_dir"])
print("Using output directory:", build_dir)
@@ -551,10 +660,12 @@ if __name__ == "__main__":
bin_dir = os.path.join(build_dir, "bin")
# directory for storing project files
sources_dir = build_dir + sources_dir
+ # copy dependency licenses to this directory
+ license_dir = os.path.join(build_dir, "license")
shutil.rmtree(build_dir, ignore_errors=True)
os.makedirs(build_dir, exist_ok=False)
- for path in [inc_dir, lib_dir, bin_dir, sources_dir]:
+ for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]:
os.makedirs(path, exist_ok=True)
prefs = {
@@ -572,6 +683,7 @@ if __name__ == "__main__":
"lib_dir": lib_dir,
"bin_dir": bin_dir,
"src_dir": sources_dir,
+ "license_dir": license_dir,
# Compilers / Tools
**msvs,
"cmake": "cmake.exe", # TODO find CMAKE automatically