Merge branch 'python-pillow'

This commit is contained in:
Andrew Murray 2022-12-31 10:19:16 +11:00
commit 6ef6973966
178 changed files with 3021 additions and 1850 deletions

View File

@ -10,7 +10,7 @@ environment:
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES
matrix: matrix:
- PYTHON: C:/Python310 - PYTHON: C:/Python311
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python37-x64 - PYTHON: C:/Python37-x64

View File

@ -13,6 +13,10 @@ indent_style = space
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.rst]
# Four-space indentation
indent_size = 4
[*.yml] [*.yml]
# Two-space indentation # Two-space indentation
indent_size = 2 indent_size = 2

View File

@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -30,7 +30,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.x"
cache: pip cache: pip
cache-dependency-path: "setup.py" cache-dependency-path: "setup.py"

View File

@ -2,7 +2,7 @@
set -e 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 PYTHONOPTIMIZE=0 python3 -m pip install cffi
python3 -m pip install coverage 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 -U pytest-timeout
python3 -m pip install pyroma 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 python3 -m pip install numpy
# extra test images # extra test images

View File

@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: "Check issues" - name: "Check issues"
uses: actions/stale@v6 uses: actions/stale@v7
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action" only-labels: "Awaiting OP Action"

View File

@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-minor-version: [7, 8, 9] python-minor-version: [8, 9]
timeout-minutes: 40 timeout-minutes: 40
@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v2 uses: cygwin/cygwin-install-action@v3
with: with:
platform: x86_64 platform: x86_64
packages: > packages: >
@ -48,7 +48,7 @@ jobs:
qt5-devel-tools subversion xorg-server-extra zlib-devel qt5-devel-tools subversion xorg-server-extra zlib-devel
- name: Add Lapack to PATH - name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v2 uses: egor-tensin/cleanup-path@v3
with: with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
@ -76,7 +76,7 @@ jobs:
- name: Build - name: Build
shell: bash.exe -eo pipefail -o igncr "{0}" shell: bash.exe -eo pipefail -o igncr "{0}"
run: | run: |
.ci/build.sh SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh
- name: Test - name: Test
run: | run: |

View File

@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -30,8 +30,8 @@ jobs:
centos-stream-9-amd64, centos-stream-9-amd64,
debian-10-buster-x86, debian-10-buster-x86,
debian-11-bullseye-x86, debian-11-bullseye-x86,
fedora-35-amd64,
fedora-36-amd64, fedora-36-amd64,
fedora-37-amd64,
gentoo, gentoo,
ubuntu-18.04-bionic-amd64, ubuntu-18.04-bionic-amd64,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,

View File

@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -15,13 +15,13 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: 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"] architecture: ["x86", "x64"]
include: include:
# PyPy 7.3.4+ only ships 64-bit binaries for Windows # PyPy 7.3.4+ only ships 64-bit binaries for Windows
- python-version: "pypy-3.7" - python-version: "pypy3.8"
architecture: "x64" architecture: "x64"
- python-version: "pypy-3.8" - python-version: "pypy3.9"
architecture: "x64" architecture: "x64"
timeout-minutes: 30 timeout-minutes: 30
@ -65,7 +65,9 @@ jobs:
xcopy /S /Y winbuild\depends\test_images\* Tests\images\ xcopy /S /Y winbuild\depends\test_images\* Tests\images\
# make cache key depend on VS version # 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 shell: pwsh
- name: Cache build - name: Cache build
@ -90,19 +92,28 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_zlib.cmd" run: "& winbuild\\build\\build_dep_zlib.cmd"
- name: Build dependencies / LibTiff - name: Build dependencies / xz
if: steps.build-cache.outputs.cache-hit != 'true' 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 - name: Build dependencies / WebP
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libwebp.cmd" 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 # for FreeType CBDT/SBIX font support
- name: Build dependencies / libpng - name: Build dependencies / libpng
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_libpng.cmd" 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 - name: Build dependencies / FreeType
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_freetype.cmd" run: "& winbuild\\build\\build_dep_freetype.cmd"
@ -130,7 +141,7 @@ jobs:
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: "& winbuild\\build\\build_dep_fribidi.cmd" run: "& winbuild\\build\\build_dep_fribidi.cmd"
# trim ~150MB x 9 # trim ~150MB for each job
- name: Optimize build cache - name: Optimize build cache
if: steps.build-cache.outputs.cache-hit != 'true' if: steps.build-cache.outputs.cache-hit != 'true'
run: rmdir /S /Q winbuild\build\src run: rmdir /S /Q winbuild\build\src
@ -185,16 +196,42 @@ jobs:
id: wheel id: wheel
if: "github.event_name != 'pull_request'" if: "github.event_name != 'pull_request'"
run: | 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 winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel
shell: cmd shell: cmd
- uses: actions/upload-artifact@v3 - name: Upload wheel
uses: actions/upload-artifact@v3
if: "github.event_name != 'pull_request'" if: "github.event_name != 'pull_request'"
with: with:
name: ${{ steps.wheel.outputs.dist }} name: ${{ steps.wheel.outputs.dist }}
path: dist\*.whl 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: success:
permissions: permissions:
contents: none contents: none

View File

@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch]
permissions: permissions:
contents: read contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -20,9 +20,9 @@ jobs:
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
"pypy-3.8", "pypy3.9",
"pypy-3.7", "pypy3.8",
"3.11-dev", "3.11",
"3.10", "3.10",
"3.9", "3.9",
"3.8", "3.8",
@ -96,7 +96,7 @@ jobs:
path: Tests/errors path: Tests/errors
- name: Docs - name: Docs
if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11
run: | run: |
make doccheck make doccheck

View File

@ -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

View File

@ -1,18 +1,25 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.8.0 rev: 22.12.0
hooks: hooks:
- id: black - id: black
args: ["--target-version", "py37"] args: [--target-version=py37]
# Only .py files, until https://github.com/psf/black/issues/402 resolved # Only .py files, until https://github.com/psf/black/issues/402 resolved
files: \.py$ files: \.py$
types: [] types: []
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.10.1 rev: 5.11.1
hooks: hooks:
- id: isort - 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 - repo: https://github.com/asottile/yesqa
rev: v1.4.0 rev: v1.4.0
hooks: hooks:
@ -25,10 +32,11 @@ repos:
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 5.0.4 rev: 6.0.0
hooks: hooks:
- id: flake8 - 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 - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0 rev: v1.9.0
@ -37,16 +45,21 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0 rev: v4.4.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json - id: check-json
- id: check-yaml - id: check-yaml
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6.1 rev: v0.6.7
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 0.5.2
hooks:
- id: tox-ini-fmt
ci: ci:
autoupdate_schedule: monthly autoupdate_schedule: monthly

View File

@ -2,9 +2,138 @@
Changelog (Pillow) 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 - Added support for reading BMP images with RLE4 compression #6674
[npjg, radarhere] [npjg, radarhere]

View File

@ -1,7 +1,6 @@
include *.c include *.c
include *.h include *.h
include *.in include *.in
include *.lock
include *.md include *.md
include *.py include *.py
include *.rst include *.rst
@ -10,7 +9,6 @@ include *.txt
include *.yaml include *.yaml
include LICENSE include LICENSE
include Makefile include Makefile
include Pipfile
include tox.ini include tox.ini
graft Tests graft Tests
graft src graft src

View File

@ -53,12 +53,12 @@ inplace: clean
.PHONY: install .PHONY: install
install: install:
python3 -m pip install . python3 -m pip -v install .
python3 selftest.py python3 selftest.py
.PHONY: install-coverage .PHONY: install-coverage
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 python3 selftest.py
.PHONY: debug .PHONY: debug
@ -67,7 +67,7 @@ debug:
# for our stuff, kills optimization, and redirects to dev null so we # for our stuff, kills optimization, and redirects to dev null so we
# see any build failures. # see any build failures.
make clean > /dev/null 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 .PHONY: release-test
release-test: release-test:

22
Pipfile
View File

@ -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"

324
Pipfile.lock generated
View File

@ -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": {}
}

View File

@ -54,9 +54,9 @@ As of 2019, Pillow development is
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a> src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml"><img <a href="https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow"><img
alt="Tidelift Align" alt="Fuzzing Status"
src="https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml/badge.svg"></a> src="https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg"></a>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -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 ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
ter-x20b.pcf, from http://terminus-font.sourceforge.net/ ter-x20b.pcf, from http://terminus-font.sourceforge.net/
BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee 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. 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.

BIN
Tests/fonts/OpenSans.woff2 Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
Tests/images/test_woff2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -19,29 +19,17 @@ python3 setup.py build --build-base=/tmp/build install
# Build fuzzers in $OUT. # Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
fuzzer_basename=$(basename -s .py $fuzzer) compile_python_fuzzer $fuzzer \
fuzzer_package=${fuzzer_basename}.pkg
pyinstaller \
--add-binary /usr/local/lib/libjpeg.so.62.3.0:. \ --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \
--add-binary /usr/local/lib/libfreetype.so.6:. \ --add-binary /usr/local/lib/libfreetype.so.6:. \
--add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \
--add-binary /usr/local/lib/libopenjp2.so.7:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \
--add-binary /usr/local/lib/libpng16.so.16:. \ --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/libwebp.so.7:. \
--add-binary /usr/local/lib/libwebpdemux.so.2:. \ --add-binary /usr/local/lib/libwebpdemux.so.2:. \
--add-binary /usr/local/lib/libwebpmux.so.3:. \ --add-binary /usr/local/lib/libwebpmux.so.3:. \
--add-binary /usr/local/lib/libxcb.so.1:. \ --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
done done
find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@ find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@

View File

@ -35,6 +35,7 @@ def test_questionable():
"pal8os2v2.bmp", "pal8os2v2.bmp",
"rgb24prof.bmp", "rgb24prof.bmp",
"pal1p1.bmp", "pal1p1.bmp",
"pal4rletrns.bmp",
"pal8offs.bmp", "pal8offs.bmp",
"rgb24lprof.bmp", "rgb24lprof.bmp",
"rgb32fakealpha.bmp", "rgb32fakealpha.bmp",

View File

@ -553,18 +553,20 @@ def test_apng_save_disposal(tmp_path):
def test_apng_save_disposal_previous(tmp_path): def test_apng_save_disposal_previous(tmp_path):
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")
size = (128, 64) 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)) red = Image.new("RGBA", size, (255, 0, 0, 255))
green = Image.new("RGBA", size, (0, 255, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255))
# test OP_NONE # test OP_NONE
transparent.save( blue.save(
test_file, test_file,
save_all=True, save_all=True,
append_images=[red, green], append_images=[red, green],
disposal=PngImagePlugin.Disposal.OP_PREVIOUS, disposal=PngImagePlugin.Disposal.OP_PREVIOUS,
) )
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
im.seek(2) im.seek(2)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255)

View File

@ -22,6 +22,8 @@ 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_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 = "Tests/images/argb-32bpp_MipMaps-1.dds"
TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.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 = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
@ -194,26 +196,24 @@ def test_unimplemented_dxgi_format():
pass pass
def test_uncompressed_rgb(): @pytest.mark.parametrize(
"""Check uncompressed RGB images can be opened""" ("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) as im:
with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im:
assert im.format == "DDS" assert im.format == "DDS"
assert im.mode == "RGB" assert im.mode == mode
assert im.size == (128, 128) assert im.size == size
assert_image_equal_tofile(im, "Tests/images/hopper.png") assert_image_equal_tofile(im, test_file.replace(".dds", ".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")
)
def test__accept_true(): def test__accept_true():
@ -305,6 +305,8 @@ def test_save_unsupported_mode(tmp_path):
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mode", "test_file"), ("mode", "test_file"),
[ [
("L", "Tests/images/linear_gradient.png"),
("LA", "Tests/images/uncompressed_la.png"),
("RGB", "Tests/images/hopper.png"), ("RGB", "Tests/images/hopper.png"),
("RGBA", "Tests/images/pil123rgba.png"), ("RGBA", "Tests/images/pil123rgba.png"),
], ],

View File

@ -677,6 +677,24 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0) 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): def test_transparency_in_second_frame(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im: with Image.open("Tests/images/different_transparency.gif") as im:
@ -791,6 +809,22 @@ def test_roundtrip_info_duration(tmp_path):
] == duration_list ] == 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): def test_identical_frames(tmp_path):
duration_list = [1000, 1500, 2000, 4000] duration_list = [1000, 1500, 2000, 4000]
@ -859,14 +893,23 @@ def test_background(tmp_path):
im.info["background"] = 1 im.info["background"] = 1
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["background"] == im.info["background"] 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"): if features.check("webp") and features.check("webp_anim"):
with Image.open("Tests/images/hopper.webp") as im: 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) 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): def test_comment(tmp_path):
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:

View File

@ -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): def test_no_duplicates(tmp_path):
temp_file = str(tmp_path / "temp.ico") temp_file = str(tmp_path / "temp.ico")
temp_file2 = str(tmp_path / "temp2.ico") temp_file2 = str(tmp_path / "temp2.ico")

View File

@ -86,6 +86,33 @@ class TestFileJpeg:
assert len(im.applist) == 2 assert len(im.applist) == 2
assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" 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): def test_cmyk(self):
# Test CMYK handling. Thanks to Tim and Charlie for test data, # Test CMYK handling. Thanks to Tim and Charlie for test data,
@ -415,6 +442,13 @@ class TestFileJpeg:
info = im._getexif() info = im._getexif()
assert info[305] == "Adobe Photoshop CS Macintosh" 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): def test_mp(self):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
assert im._getmp() is None assert im._getmp() is None

View File

@ -252,6 +252,20 @@ def test_mct():
assert_image_similar(im, jp2, 1.0e-3) 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(): def test_rgba():
# Arrange # Arrange
with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:

View File

@ -3,6 +3,7 @@ import io
import itertools import itertools
import os import os
import re import re
import sys
from collections import namedtuple from collections import namedtuple
import pytest import pytest
@ -825,6 +826,44 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.mode == "F" assert reloaded.mode == "F"
assert reloaded.getexif()[SAMPLEFORMAT] == 3 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): def test_lzw(self):
with Image.open("Tests/images/hopper_lzw.tif") as im: with Image.open("Tests/images/hopper_lzw.tif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"

View File

@ -80,7 +80,10 @@ def test_app(test_file):
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_exif(test_file): 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() info = im._getexif()
assert info[272] == "Nintendo 3DS" assert info[272] == "Nintendo 3DS"
assert info[296] == 2 assert info[296] == 2
@ -268,6 +271,7 @@ def test_save_all():
im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded) assert_image_equal(im, im_reloaded)
assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1) im_reloaded.seek(1)
assert_image_similar(im2, im_reloaded, 1) assert_image_similar(im2, im_reloaded, 1)

View File

@ -42,7 +42,6 @@ def test_save(tmp_path, mode):
helper_save_as_pdf(tmp_path, mode) helper_save_as_pdf(tmp_path, mode)
@pytest.mark.valgrind_known_error(reason="Temporary skip")
def test_monochrome(tmp_path): def test_monochrome(tmp_path):
# Arrange # Arrange
mode = "1" mode = "1"

View File

@ -706,10 +706,18 @@ class TestFilePng:
assert exif[274] == 3 assert exif[274] == 3
def test_exif_save(self, tmp_path): 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: with Image.open("Tests/images/exif.png") as im:
test_file = str(tmp_path / "temp.png")
im.save(test_file) 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: with Image.open(test_file) as reloaded:
exif = reloaded._getexif() exif = reloaded._getexif()
assert exif[274] == 1 assert exif[274] == 1
@ -720,7 +728,7 @@ class TestFilePng:
def test_exif_from_jpg(self, tmp_path): def test_exif_from_jpg(self, tmp_path):
with Image.open("Tests/images/pil_sample_rgb.jpg") as im: with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
test_file = str(tmp_path / "temp.png") 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: with Image.open(test_file) as reloaded:
exif = reloaded._getexif() exif = reloaded._getexif()

View File

@ -4,7 +4,7 @@ from io import BytesIO
import pytest 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 PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
from .helper import ( from .helper import (
@ -858,6 +858,19 @@ class TestFileTiff:
im.load() im.load()
ImageFile.LOAD_TRUNCATED_IMAGES = False 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") @pytest.mark.skipif(not is_win32(), reason="Windows only")
class TestFileTiffW32: class TestFileTiffW32:

View File

@ -185,20 +185,37 @@ def test_iptc(tmp_path):
im.save(out) im.save(out)
def test_writing_bytes_to_ascii(tmp_path): @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
im = hopper() def test_writing_other_types_to_ascii(value, expected, tmp_path):
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271] tag = TiffTags.TAGS_V2[271]
assert tag.type == TiffTags.ASCII 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") out = str(tmp_path / "temp.tiff")
im.save(out, tiffinfo=info) im.save(out, tiffinfo=info)
with Image.open(out) as reloaded: 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): def test_undefined_zero(tmp_path):

View File

@ -97,6 +97,35 @@ def test_write_rgba(tmp_path):
assert_image_similar(image, pil_image, 1.0) 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): def test_write_unsupported_mode_PA(tmp_path):
""" """
Saving a palette-based file with transparency to WebP format Saving a palette-based file with transparency to WebP format

View File

@ -11,6 +11,11 @@ pytestmark = [
skip_unless_feature("webp_mux"), skip_unless_feature("webp_mux"),
] ]
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
def test_read_exif_metadata(): def test_read_exif_metadata():
@ -110,6 +115,22 @@ def test_read_no_exif():
assert not webp_image._getexif() 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") @skip_unless_feature("webp_anim")
def test_write_animated_metadata(tmp_path): def test_write_animated_metadata(tmp_path):
iccp_data = b"<iccp_data>" iccp_data = b"<iccp_data>"

View File

@ -7,7 +7,14 @@ import warnings
import pytest import pytest
from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features from PIL import (
ExifTags,
Image,
ImageDraw,
ImagePalette,
UnidentifiedImageError,
features,
)
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -394,8 +401,6 @@ class TestImage:
def test_registered_extensions_uninitialized(self): def test_registered_extensions_uninitialized(self):
# Arrange # Arrange
Image._initialized = 0 Image._initialized = 0
extension = Image.EXTENSION
Image.EXTENSION = {}
# Act # Act
Image.registered_extensions() Image.registered_extensions()
@ -403,10 +408,6 @@ class TestImage:
# Assert # Assert
assert Image._initialized == 2 assert Image._initialized == 2
# Restore the original state and assert
Image.EXTENSION = extension
assert Image.EXTENSION
def test_registered_extensions(self): def test_registered_extensions(self):
# Arrange # Arrange
# Open an image to trigger plugin registration # Open an image to trigger plugin registration
@ -808,6 +809,18 @@ class TestImage:
reloaded_exif.load(exif.tobytes()) reloaded_exif.load(exif.tobytes())
assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) 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): def test_exif_ifd(self):
with Image.open("Tests/images/flower.jpg") as im: with Image.open("Tests/images/flower.jpg") as im:
exif = im.getexif() exif = im.getexif()
@ -838,6 +851,31 @@ class TestImage:
34665: 196, 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))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero_tobytes(self, size): def test_zero_tobytes(self, size):
im = Image.new("RGB", size) im = Image.new("RGB", size)

View File

@ -4,11 +4,10 @@ import sys
import sysconfig import sysconfig
import pytest import pytest
from setuptools.command.build_ext import new_compiler
from PIL import Image 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 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
@ -406,14 +405,13 @@ class TestImagePutPixelError(AccessTest):
class TestEmbeddable: class TestEmbeddable:
@pytest.mark.skipif( @pytest.mark.xfail(reason="failing test")
not is_win32() or on_ci(), @pytest.mark.skipif(not is_win32(), reason="requires Windows")
reason="Failing on AppVeyor / GitHub Actions when run from subprocess, "
"not from shell",
)
def test_embeddable(self): def test_embeddable(self):
import ctypes import ctypes
from setuptools.command.build_ext import new_compiler
with open("embed_pil.c", "w", encoding="utf-8") as fh: with open("embed_pil.c", "w", encoding="utf-8") as fh:
fh.write( fh.write(
""" """

View File

@ -104,6 +104,13 @@ def test_rgba_p():
assert_image_similar(im, comparable, 20) 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): def test_trns_p(tmp_path):
im = hopper("P") im = hopper("P")
im.info["transparency"] = 0 im.info["transparency"] = 0

View File

@ -55,10 +55,11 @@ def test_mode_with_L_with_float():
assert im.getpixel((0, 0)) == 2 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") src = hopper("L")
data = list(src.getdata()) data = list(src.getdata())
im = Image.new("I", src.size, 0) im = Image.new(mode, src.size, 0)
im.putdata(data, 2, 256) im.putdata(data, 2, 256)
target = [2 * elt + 256 for elt in data] target = [2 * elt + 256 for elt in data]

View File

@ -1238,6 +1238,27 @@ def test_stroke_descender():
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) 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") @skip_unless_feature("freetype2")
def test_stroke_multiline(): def test_stroke_multiline():
# Arrange # Arrange

View File

@ -746,12 +746,14 @@ def test_variation_set_by_name(font):
_check_text(font, "Tests/images/variation_adobe.png", 11) _check_text(font, "Tests/images/variation_adobe.png", 11)
for name in ["Bold", b"Bold"]: for name in ["Bold", b"Bold"]:
font.set_variation_by_name(name) 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) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36)
_check_text(font, "Tests/images/variation_tiny.png", 40) _check_text(font, "Tests/images/variation_tiny.png", 40)
for name in ["200", b"200"]: for name in ["200", b"200"]:
font.set_variation_by_name(name) font.set_variation_by_name(name)
assert font.getname()[1] == "200"
_check_text(font, "Tests/images/variation_tiny_name.png", 40) _check_text(font, "Tests/images/variation_tiny_name.png", 40)
@ -1063,6 +1065,25 @@ def test_colr_mask(layout_engine):
assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) 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): def test_fill_deprecation(font):
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
font.getmask2("Hello world", fill=Image.core.fill) font.getmask2("Hello world", fill=Image.core.fill)

View File

@ -1,4 +1,5 @@
import os import os
import shutil
import subprocess import subprocess
import sys import sys
@ -33,7 +34,9 @@ class TestImageGrab:
@pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB")
def test_grab_no_xcb(self): 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: with pytest.raises(OSError) as e:
ImageGrab.grab() ImageGrab.grab()
assert str(e.value).startswith("Pillow was built without XCB support") assert str(e.value).startswith("Pillow was built without XCB support")
@ -61,9 +64,13 @@ $bmp = New-Object Drawing.Bitmap 200, 200
) )
p.communicate() p.communicate()
else: else:
with pytest.raises(NotImplementedError) as e: if not shutil.which("wl-paste"):
ImageGrab.grabclipboard() with pytest.raises(
assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" NotImplementedError,
match="wl-paste or xclip is required for"
r" ImageGrab.grabclipboard\(\) on Linux",
):
ImageGrab.grabclipboard()
return return
ImageGrab.grabclipboard() ImageGrab.grabclipboard()

View File

@ -34,7 +34,7 @@ def test_numpy_to_image():
# Check supported 1-bit integer formats # Check supported 1-bit integer formats
assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE) 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 # Check supported 8-bit integer formats
assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE)
@ -193,7 +193,7 @@ def test_putdata():
"dtype", "dtype",
( (
bool, bool,
numpy.bool8, numpy.bool_,
numpy.int8, numpy.int8,
numpy.int16, numpy.int16,
numpy.int32, numpy.int32,

View File

@ -15,11 +15,12 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others # the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext .PHONY: help
help: help:
@echo "Please use \`make <target>' where <target> is one of" @echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files" @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 " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file" @echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files" @echo " pickle to make pickle files"
@ -39,42 +40,49 @@ help:
@echo " linkcheck to check all external links for integrity" @echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " doctest to run all doctests embedded in the documentation (if enabled)"
.PHONY: clean
clean: clean:
-rm -rf $(BUILDDIR)/* -rm -rf $(BUILDDIR)/*
install-sphinx: 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: html:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo @echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml: dirhtml:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo @echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml: singlehtml:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo @echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle: pickle:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo @echo
@echo "Build finished; now you can process the pickle files." @echo "Build finished; now you can process the pickle files."
.PHONY: json
json: json:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo @echo
@echo "Build finished; now you can process the JSON files." @echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp: htmlhelp:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@ -82,6 +90,7 @@ htmlhelp:
@echo "Build finished; now you can run HTML Help Workshop with the" \ @echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp." ".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp: qthelp:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@ -92,6 +101,7 @@ qthelp:
@echo "To view the help file:" @echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc"
.PHONY: devhelp
devhelp: devhelp:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@ -102,12 +112,14 @@ devhelp:
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork"
@echo "# devhelp" @echo "# devhelp"
.PHONY: epub
epub: epub:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo @echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub." @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: latex
latex: latex:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@ -116,6 +128,7 @@ latex:
@echo "Run \`make' in that directory to run these through (pdf)latex" \ @echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)." "(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf: latexpdf:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@ -123,18 +136,21 @@ latexpdf:
$(MAKE) -C $(BUILDDIR)/latex all-pdf $(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text: text:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo @echo
@echo "Build finished. The text files are in $(BUILDDIR)/text." @echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man: man:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo @echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man." @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo: texinfo:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@ -143,6 +159,7 @@ texinfo:
@echo "Run \`make' in that directory to run these through makeinfo" \ @echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)." "(use \`make info' here to do that automatically)."
.PHONY: info
info: info:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@ -150,18 +167,21 @@ info:
make -C $(BUILDDIR)/texinfo info make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext: gettext:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo @echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes: changes:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo @echo
@echo "The overview file is in $(BUILDDIR)/changes." @echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck: linkcheck:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto $(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 " \ @echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt." "or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest: doctest:
$(MAKE) install-sphinx $(MAKE) install-sphinx
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \ @echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt." "results in $(BUILDDIR)/doctest/output.txt."
.PHONY: livehtml
livehtml: html livehtml: html
livereload $(BUILDDIR)/html -p 33233 livereload $(BUILDDIR)/html -p 33233
.PHONY: serve
serve: serve:
cd $(BUILDDIR)/html; $(PYTHON) -m http.server cd $(BUILDDIR)/html; $(PYTHON) -m http.server

View File

@ -27,12 +27,13 @@ needs_sphinx = "2.4"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
"sphinx_copybutton",
"sphinx_issues",
"sphinx_removed_in",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.intersphinx", "sphinx.ext.intersphinx",
"sphinx.ext.viewcode", "sphinx.ext.viewcode",
"sphinx_copybutton",
"sphinx_inline_tabs",
"sphinx_issues",
"sphinx_removed_in",
"sphinxext.opengraph", "sphinxext.opengraph",
] ]

View File

@ -211,13 +211,16 @@ class DdsImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
if not _accept(self.fp.read(4)): 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("<I", self.fp.read(4)) (header_size,) = struct.unpack("<I", self.fp.read(4))
if header_size != 124: if header_size != 124:
raise OSError(f"Unsupported header size {repr(header_size)}") msg = f"Unsupported header size {repr(header_size)}"
raise OSError(msg)
header_bytes = self.fp.read(header_size - 4) header_bytes = self.fp.read(header_size - 4)
if len(header_bytes) != 120: if len(header_bytes) != 120:
raise OSError(f"Incomplete header: {len(header_bytes)} bytes") msg = f"Incomplete header: {len(header_bytes)} bytes"
raise OSError(msg)
header = BytesIO(header_bytes) header = BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12)) flags, height, width = struct.unpack("<3I", header.read(12))
@ -237,7 +240,8 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"DXT5": elif fourcc == b"DXT5":
self.decoder = "DXT5" self.decoder = "DXT5"
else: else:
raise NotImplementedError(f"Unimplemented pixel format {fourcc}") msg = f"Unimplemented pixel format {fourcc}"
raise NotImplementedError(msg)
self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
@ -252,7 +256,8 @@ class DXT1Decoder(ImageFile.PyDecoder):
try: try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
raise OSError("Truncated DDS file") from e msg = "Truncated DDS file"
raise OSError(msg) from e
return -1, 0 return -1, 0
@ -263,7 +268,8 @@ class DXT5Decoder(ImageFile.PyDecoder):
try: try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e: except struct.error as e:
raise OSError("Truncated DDS file") from e msg = "Truncated DDS file"
raise OSError(msg) from e
return -1, 0 return -1, 0

View File

@ -24,9 +24,10 @@ To get the number and names of bands in an image, use the
Modes Modes
----- -----
The ``mode`` of an image is a string which defines the type and depth of a pixel in the image. The ``mode`` of an image is a string which defines the type and depth of a pixel in the
Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a range of
of 0-1, an 8-bit pixel has a range of 0-255 and so on. The current release 0-1, an 8-bit pixel has a range of 0-255, a 32-signed integer pixel has the range of
INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release
supports the following standard modes: supports the following standard modes:
* ``1`` (1-bit pixels, black and white, stored with one pixel per byte) * ``1`` (1-bit pixels, black and white, stored with one pixel per byte)
@ -41,6 +42,9 @@ supports the following standard modes:
* ``LAB`` (3x8-bit pixels, the L*a*b color space) * ``LAB`` (3x8-bit pixels, the L*a*b color space)
* ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space)
* Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees
* ``I`` (32-bit signed integer pixels) * ``I`` (32-bit signed integer pixels)
* ``F`` (32-bit floating point pixels) * ``F`` (32-bit floating point pixels)
@ -60,6 +64,12 @@ Pillow also provides limited support for a few additional modes, including:
* ``BGR;24`` (24-bit reversed true colour) * ``BGR;24`` (24-bit reversed true colour)
* ``BGR;32`` (32-bit reversed true colour) * ``BGR;32`` (32-bit reversed true colour)
Premultiplied alpha is where the values for each other channel have been
multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)``
would convert to an RGBa pixel of ``(5, 10, 15, 127)``. The values of the R,
G and B channels are halved as a result of the half transparency in the alpha
channel.
Apart from these additional modes, Pillow doesn't yet support multichannel Apart from these additional modes, Pillow doesn't yet support multichannel
images with a depth of more than 8 bits per channel. images with a depth of more than 8 bits per channel.
@ -107,6 +117,18 @@ the file format handler (see the chapter on :ref:`image-file-formats`). Most
handlers add properties to the :py:attr:`~PIL.Image.Image.info` attribute when handlers add properties to the :py:attr:`~PIL.Image.Image.info` attribute when
loading an image, but ignore it when saving images. loading an image, but ignore it when saving images.
Transparency
------------
If an image does not have an alpha band, transparency may be specified in the
:py:attr:`~PIL.Image.Image.info` attribute with a "transparency" key.
Most of the time, the "transparency" value is a single integer, describing
which pixel value is transparent in a "1", "L", "I" or "P" mode image.
However, PNG images may have three values, one for each channel in an "RGB"
mode image, or can have a byte string for a "P" mode image, to specify the
alpha value for each palette entry.
Orientation Orientation
----------- -----------

View File

@ -474,6 +474,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
.. versionadded:: 2.5.0 .. versionadded:: 2.5.0
**comment**
A comment about the image.
.. versionadded:: 9.4.0
.. note:: .. note::
@ -563,6 +568,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
encoded using RLCP mode will have increasing resolutions decoded as they encoded using RLCP mode will have increasing resolutions decoded as they
arrive, and so on. arrive, and so on.
**signed**
If true, then tell the encoder to save the image as signed.
.. versionadded:: 9.4.0
**cinema_mode** **cinema_mode**
Set the encoder to produce output compliant with the digital cinema Set the encoder to produce output compliant with the digital cinema
specifications. The options here are ``"no"`` (the default), specifications. The options here are ``"no"`` (the default),
@ -1124,6 +1134,11 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
**method** **method**
Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4.
**exact**
If true, preserve the transparent RGB values. Otherwise, discard
invisible RGB values for better compression. Defaults to false.
Requires libwebp 0.5.0 or later.
**icc_profile** **icc_profile**
The ICC Profile to include in the saved file. Only supported if The ICC Profile to include in the saved file. Only supported if
the system WebP library was built with webpmux support. the system WebP library was built with webpmux support.

View File

@ -78,7 +78,8 @@ true color.
elif bits == 24: elif bits == 24:
self.mode = "RGB" self.mode = "RGB"
else: else:
raise SyntaxError("unknown number of bits") msg = "unknown number of bits"
raise SyntaxError(msg)
# data descriptor # data descriptor
self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]

View File

@ -57,9 +57,9 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge
:alt: Tidelift :alt: Tidelift
.. image:: https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml/badge.svg .. image:: https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/tidelift.yml :target: https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow
:alt: Tidelift Align :alt: Fuzzing Status
.. image:: https://img.shields.io/pypi/v/pillow.svg .. image:: https://img.shields.io/pypi/v/pillow.svg
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/Pillow/

View File

@ -23,6 +23,11 @@ Pillow supports these Python versions.
:file: older-versions.csv :file: older-versions.csv
:header-rows: 1 :header-rows: 1
.. _Linux Installation:
.. _macOS Installation:
.. _Windows Installation:
.. _FreeBSD Installation:
Basic Installation Basic Installation
------------------ ------------------
@ -38,75 +43,73 @@ Install Pillow with :command:`pip`::
python3 -m pip install --upgrade Pillow python3 -m pip install --upgrade Pillow
Windows Installation .. tab:: Linux
^^^^^^^^^^^^^^^^^^^^
We provide Pillow binaries for Windows compiled for the matrix of We provide binaries for Linux for each of the supported Python
supported Pythons in both 32 and 64-bit versions in the wheel format. versions in the manylinux wheel format. These include support for all
These binaries include support for all optional libraries except optional libraries except libimagequant. Raqm support requires
libimagequant and libxcb. Raqm support requires FriBiDi to be installed separately::
FriBiDi to be installed separately::
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow python3 -m pip install --upgrade Pillow
To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. 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``.
.. tab:: macOS
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
.. tab:: Windows
We provide Pillow binaries for Windows compiled for the matrix of
supported Pythons in both 32 and 64-bit versions in the wheel format.
These binaries include support for all optional libraries except
libimagequant and libxcb. Raqm support requires
FriBiDi to be installed separately::
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade Pillow
To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_.
.. tab:: FreeBSD
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
<https://www.freshports.org/graphics/py-pillow/>`_ and packages
are tested by the ports team with all supported FreeBSD versions.
macOS Installation .. _Building on Linux:
^^^^^^^^^^^^^^^^^^ .. _Building on macOS:
.. _Building on Windows:
We provide binaries for macOS for each of the supported Python .. _Building on Windows using MSYS2/MinGW:
versions in the wheel format. These include support for all optional .. _Building on FreeBSD:
libraries except libimagequant. Raqm support requires .. _Building on Android:
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
<https://www.freshports.org/graphics/py-pillow/>`_ and packages
are tested by the ports team with all supported FreeBSD versions.
Building From Source Building From Source
-------------------- --------------------
Download and extract the `compressed archive from PyPI`_.
.. _compressed archive from PyPI: https://pypi.org/project/Pillow/
.. _external-libraries: .. _external-libraries:
External Libraries External Libraries
@ -140,14 +143,14 @@ Many of Pillow's features require external libraries:
* **libtiff** provides compressed TIFF functionality * **libtiff** provides compressed TIFF functionality
* Pillow has been tested with libtiff versions **3.x** and **4.0-4.4** * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5**
* **libfreetype** provides type related services * **libfreetype** provides type related services
* **littlecms** provides color management * **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and * 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. * **libwebp** provides the WebP format.
@ -191,7 +194,141 @@ Many of Pillow's features require external libraries:
* **libxcb** provides X11 screengrab support. * **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
<https://brew.sh/>`_. 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 pip
python3 -m pip install --upgrade Pillow --no-binary :all: 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 without cache using the ``--no-cache-dir`` option to force a
build with newly installed external libraries. 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 Build Options
^^^^^^^^^^^^^ """""""""""""
* Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use * Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use
multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` 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]" 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
<https://brew.sh/>`_. 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 Platform Support
---------------- ----------------
@ -440,10 +436,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 11 Bullseye | 3.9 | x86 | | Debian 11 Bullseye | 3.9 | x86 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 35 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 36 | 3.10 | x86-64 | | Fedora 36 | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 37 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 | | Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | 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.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 | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | 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 | | 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 | | | | 3.6 | 8.4.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+

View File

@ -4,14 +4,56 @@
:py:mod:`~PIL.ExifTags` Module :py:mod:`~PIL.ExifTags` Module
============================== ==============================
The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes
provide constants and clear-text names for various well-known EXIF tags. 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 .. py:data:: TAGS
:type: dict :type: dict
The TAGS dictionary maps 16-bit integer EXIF tag enumerations to 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 >>> from PIL.ExifTags import TAGS
>>> TAGS[0x010e] >>> TAGS[0x010e]
@ -20,8 +62,8 @@ provide constants and clear-text names for various well-known EXIF tags.
.. py:data:: GPSTAGS .. py:data:: GPSTAGS
:type: dict :type: dict
The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to The GPSTAGS dictionary maps 8-bit integer EXIF GPS enumerations to
descriptive string names. For instance: descriptive string names. For instance:
>>> from PIL.ExifTags import GPSTAGS >>> from PIL.ExifTags import GPSTAGS
>>> GPSTAGS[20] >>> GPSTAGS[20]

View File

@ -139,17 +139,50 @@ Functions
must be the same as the image mode. If omitted, the mode must be the same as the image mode. If omitted, the mode
defaults to the mode of the image. 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 Methods
------- -------
.. py:method:: ImageDraw.getfont() .. 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:: If the current default font is ``None``,
it is initialized with :py:func:`.ImageFont.load_default`.
from PIL import ImageDraw, ImageFont
ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
:returns: An image font. :returns: An image font.

View File

@ -1,9 +1,6 @@
9.3.0 9.3.0
----- -----
Backwards Incompatible Changes
==============================
API Additions API Additions
============= =============
@ -32,10 +29,23 @@ Additional images can also be appended when saving, by combining the
im.save(out, save_all=True, append_images=[im1, im2, ...]) 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 Security
======== ========
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 Decode JPEG compressed BLP1 data in original mode
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -43,18 +53,55 @@ 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 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 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 the channels afterwards. Trying to load the data in an incorrect mode could
result in a segmentation fault. 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.
Other Changes Other Changes
============= =============
Python 3.11 wheels
^^^^^^^^^^^^^^^^^^
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 <https://www.cgohlke.com/>`_.
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 Added DDS ATI1, ATI2 and BC6H reading
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images. 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 Show all frames with ImageShow
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When calling :py:meth:`~PIL.Image.Image.show` or using When calling :py:meth:`~PIL.Image.Image.show` or using
:py:mod:`~PIL.ImageShow`, all frames will now be shown. :py:mod:`~PIL.ImageShow`, all frames will now be shown.
.. _OSS-Fuzz: https://github.com/google/oss-fuzz

111
docs/releasenotes/9.4.0.rst Normal file
View File

@ -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 <https://en.wikipedia.org/wiki/Extensible_Metadata_Platform>`_ 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.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
9.4.0
9.3.0 9.3.0
9.2.0 9.2.0
9.1.1 9.1.1

View File

@ -46,6 +46,7 @@ docs =
olefile olefile
sphinx>=2.4 sphinx>=2.4
sphinx-copybutton sphinx-copybutton
sphinx-inline-tabs
sphinx-issues>=3.0.1 sphinx-issues>=3.0.1
sphinx-removed-in sphinx-removed-in
sphinxext-opengraph sphinxext-opengraph

View File

@ -15,9 +15,7 @@ import subprocess
import sys import sys
import warnings import warnings
from setuptools import Extension from setuptools import Extension, setup
from setuptools import __version__ as setuptools_version
from setuptools import setup
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
@ -364,15 +362,15 @@ class pil_build_ext(build_ext):
self.feature.required.discard(x) self.feature.required.discard(x)
_dbg("Disabling %s", x) _dbg("Disabling %s", x)
if getattr(self, f"enable_{x}"): if getattr(self, f"enable_{x}"):
raise ValueError( msg = f"Conflicting options: --enable-{x} and --disable-{x}"
f"Conflicting options: --enable-{x} and --disable-{x}" raise ValueError(msg)
)
if x == "freetype": if x == "freetype":
_dbg("--disable-freetype implies --disable-raqm") _dbg("--disable-freetype implies --disable-raqm")
if getattr(self, "enable_raqm"): if getattr(self, "enable_raqm"):
raise ValueError( msg = (
"Conflicting options: --enable-raqm and --disable-freetype" "Conflicting options: --enable-raqm and --disable-freetype"
) )
raise ValueError(msg)
setattr(self, "disable_raqm", True) setattr(self, "disable_raqm", True)
if getattr(self, f"enable_{x}"): if getattr(self, f"enable_{x}"):
_dbg("Requiring %s", x) _dbg("Requiring %s", x)
@ -383,13 +381,11 @@ class pil_build_ext(build_ext):
for x in ("raqm", "fribidi"): for x in ("raqm", "fribidi"):
if getattr(self, f"vendor_{x}"): if getattr(self, f"vendor_{x}"):
if getattr(self, "disable_raqm"): if getattr(self, "disable_raqm"):
raise ValueError( msg = f"Conflicting options: --vendor-{x} and --disable-raqm"
f"Conflicting options: --vendor-{x} and --disable-raqm" raise ValueError(msg)
)
if x == "fribidi" and not getattr(self, "vendor_raqm"): if x == "fribidi" and not getattr(self, "vendor_raqm"):
raise ValueError( msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm"
f"Conflicting options: --vendor-{x} and not --vendor-raqm" raise ValueError(msg)
)
_dbg("Using vendored version of %s", x) _dbg("Using vendored version of %s", x)
self.feature.vendor.add(x) self.feature.vendor.add(x)
@ -852,7 +848,6 @@ class pil_build_ext(build_ext):
sys.platform == "win32" sys.platform == "win32"
and sys.version_info < (3, 9) and sys.version_info < (3, 9)
and not (PLATFORM_PYPY or PLATFORM_MINGW) and not (PLATFORM_PYPY or PLATFORM_MINGW)
and int(setuptools_version.split(".")[0]) < 60
): ):
defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""'))
else: else:

View File

@ -22,7 +22,8 @@ def _save_frame(im: Image.Image, fp: BytesIO, filename: str, info: dict):
h = info.get("hotspots", [(0, 0) for _ in range(len(s))]) h = info.get("hotspots", [(0, 0) for _ in range(len(s))])
if len(h) != len(s): if len(h) != len(s):
raise ValueError("Number of hotspots must be equal to number of cursor sizes") msg = "Number of hotspots must be equal to number of cursor sizes"
raise ValueError(msg)
# sort and remove duplicate sizes # sort and remove duplicate sizes
sizes, hotspots = [], [] sizes, hotspots = [], []
@ -154,7 +155,8 @@ def _write_multiple_frames(im: Image.Image, fp: BytesIO, filename: str):
seq_offset = fp.tell() seq_offset = fp.tell()
for i in seq: for i in seq:
if i >= len(frames): if i >= len(frames):
raise ValueError("Sequence index out of animation frame bounds") msg = "Sequence index out of animation frame bounds"
raise ValueError(msg)
fp.write(o32(i)) fp.write(o32(i))
@ -172,10 +174,12 @@ def _write_multiple_frames(im: Image.Image, fp: BytesIO, filename: str):
if seq: if seq:
if len(rate) != len(seq): if len(rate) != len(seq):
raise ValueError("Length of rate must match length of sequence") msg = "Length of rate must match length of sequence"
raise ValueError(msg)
else: else:
if len(rate) != len(frames): if len(rate) != len(frames):
raise ValueError("Length of rate must match number of frames") msg = "Length of rate must match number of frames"
raise ValueError(msg)
for r in rate: for r in rate:
fp.write(o32(r)) fp.write(o32(r))
@ -220,12 +224,14 @@ def _write_info(im: Image.Image, fp: BytesIO, filename: str):
if isinstance(inam, str): if isinstance(inam, str):
inam = inam.encode() inam = inam.encode()
if not isinstance(inam, bytes): if not isinstance(inam, bytes):
raise TypeError("'inam' argument must be either a string or bytes") msg = "'inam' argument must be either a string or bytes"
raise TypeError(msg)
if isinstance(iart, str): if isinstance(iart, str):
iart = iart.encode() iart = iart.encode()
if not isinstance(iart, bytes): if not isinstance(iart, bytes):
raise TypeError("'iart' argument must be either a string or bytes") msg = "'iart' argument must be either a string or bytes"
raise TypeError(msg)
fp.write(b"INFO") fp.write(b"INFO")
fp.write(b"INAM" + o32(0)) fp.write(b"INAM" + o32(0))
@ -335,7 +341,8 @@ class AniFile:
listOffset = listOffset + lSize + 8 listOffset = listOffset + lSize + 8
if self.anih is None: if self.anih is None:
raise SyntaxError("not an ANI file") msg = "not an ANI file"
raise SyntaxError(msg)
if self.seq is None: if self.seq is None:
self.seq = [i for i in range(self.anih["nFrames"])] self.seq = [i for i in range(self.anih["nFrames"])]
@ -345,7 +352,8 @@ class AniFile:
def frame(self, frame): def frame(self, frame):
if frame > self.anih["nFrames"]: if frame > self.anih["nFrames"]:
raise EOFError("Frame index out of animation bounds") msg = "Frame index out of animation bounds"
raise EOFError(msg)
offset, size = self.image_data[frame].values() offset, size = self.image_data[frame].values()
self.buf.seek(offset) self.buf.seek(offset)
@ -406,7 +414,8 @@ class AniImageFile(ImageFile.ImageFile):
@size.setter @size.setter
def size(self, value): def size(self, value):
if value not in self.info["sizes"]: 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 self._size = value
def load(self): def load(self):
@ -419,7 +428,8 @@ class AniImageFile(ImageFile.ImageFile):
def seek(self, frame): def seek(self, frame):
if frame > self.info["frames"] - 1 or frame < 0: if frame > self.info["frames"] - 1 or frame < 0:
raise EOFError("Frame index out of animation bounds") msg = "Frame index out of animation bounds"
raise EOFError(msg)
self.frame = frame self.frame = frame
self.load() self.load()

View File

@ -86,7 +86,8 @@ class BdfFontFile(FontFile.FontFile):
s = fp.readline() s = fp.readline()
if s[:13] != b"STARTFONT 2.1": if s[:13] != b"STARTFONT 2.1":
raise SyntaxError("not a valid BDF file") msg = "not a valid BDF file"
raise SyntaxError(msg)
props = {} props = {}
comments = [] comments = []

View File

@ -65,7 +65,8 @@ def __getattr__(name):
if name in enum.__members__: if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[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): def unpack_565(i):
@ -278,7 +279,8 @@ class BlpImageFile(ImageFile.ImageFile):
if self.magic in (b"BLP1", b"BLP2"): if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode() decoder = self.magic.decode()
else: 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.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
@ -292,7 +294,8 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
self._read_blp_header() self._read_blp_header()
self._load() self._load()
except struct.error as e: except struct.error as e:
raise OSError("Truncated BLP file") from e msg = "Truncated BLP file"
raise OSError(msg) from e
return -1, 0 return -1, 0
def _read_blp_header(self): def _read_blp_header(self):
@ -354,13 +357,11 @@ class BLP1Decoder(_BLPBaseDecoder):
data = self._read_bgra(palette) data = self._read_bgra(palette)
self.set_as_raw(bytes(data)) self.set_as_raw(bytes(data))
else: else:
raise BLPFormatError( msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}"
f"Unsupported BLP encoding {repr(self._blp_encoding)}" raise BLPFormatError(msg)
)
else: else:
raise BLPFormatError( msg = f"Unsupported BLP compression {repr(self._blp_encoding)}"
f"Unsupported BLP compression {repr(self._blp_encoding)}" raise BLPFormatError(msg)
)
def _decode_jpeg_stream(self): def _decode_jpeg_stream(self):
from .JpegImagePlugin import JpegImageFile from .JpegImagePlugin import JpegImageFile
@ -373,6 +374,9 @@ class BLP1Decoder(_BLPBaseDecoder):
data = BytesIO(data) data = BytesIO(data)
image = JpegImageFile(data) image = JpegImageFile(data)
Image._decompression_bomb_check(image.size) Image._decompression_bomb_check(image.size)
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() r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r)) image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes()) self.set_as_raw(image.tobytes())
@ -412,16 +416,15 @@ class BLP2Decoder(_BLPBaseDecoder):
for d in decode_dxt5(self._safe_read(linesize)): for d in decode_dxt5(self._safe_read(linesize)):
data += d data += d
else: else:
raise BLPFormatError( msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" raise BLPFormatError(msg)
)
else: else:
raise BLPFormatError(f"Unknown BLP encoding {repr(self._blp_encoding)}") msg = f"Unknown BLP encoding {repr(self._blp_encoding)}"
raise BLPFormatError(msg)
else: else:
raise BLPFormatError( msg = f"Unknown BLP compression {repr(self._blp_compression)}"
f"Unknown BLP compression {repr(self._blp_compression)}" raise BLPFormatError(msg)
)
self.set_as_raw(bytes(data)) self.set_as_raw(bytes(data))
@ -457,7 +460,8 @@ class BLPEncoder(ImageFile.PyEncoder):
def _save(im, fp, filename, save_all=False): def _save(im, fp, filename, save_all=False):
if im.mode != "P": 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" magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic) fp.write(magic)

View File

@ -146,7 +146,8 @@ class BmpImageFile(ImageFile.ImageFile):
file_info["a_mask"], file_info["a_mask"],
) )
else: 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 # ------------------ Special case : header is reported 40, which
# ---------------------- is shorter than real size for bpp >= 16 # ---------------------- is shorter than real size for bpp >= 16
@ -164,7 +165,8 @@ class BmpImageFile(ImageFile.ImageFile):
# ---------------------- Check bit depth for unusual unsupported values # ---------------------- Check bit depth for unusual unsupported values
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None: if self.mode is None:
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) # ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw" decoder_name = "raw"
@ -205,9 +207,11 @@ class BmpImageFile(ImageFile.ImageFile):
): ):
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else: else:
raise OSError("Unsupported BMP bitfields layout") msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
else: else:
raise OSError("Unsupported BMP bitfields layout") msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
elif file_info["compression"] == self.RAW: elif file_info["compression"] == self.RAW:
try: try:
if file_info["bits"] == 32 and self.alpha: if file_info["bits"] == 32 and self.alpha:
@ -217,14 +221,16 @@ class BmpImageFile(ImageFile.ImageFile):
elif file_info["compression"] in (self.RLE8, self.RLE4): elif file_info["compression"] in (self.RLE8, self.RLE4):
decoder_name = "bmp_rle" decoder_name = "bmp_rle"
else: else:
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 # --------------- Once the header is processed, process the palette/LUT
if self.mode == "P": # Paletted for 1, 4 and 8 bit images if self.mode == "P": # Paletted for 1, 4 and 8 bit images
# ---------------------------------------------------- 1-bit images # ---------------------------------------------------- 1-bit images
if not (0 < file_info["colors"] <= 65536): 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: else:
padding = file_info["palette_padding"] padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"]) palette = read(padding * file_info["colors"])
@ -274,7 +280,8 @@ class BmpImageFile(ImageFile.ImageFile):
head_data = self.fp.read(14) head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes # choke if the file does not have the required magic bytes
if not _accept(head_data): 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) # read the start position of the BMP image data (u32)
offset = i32(head_data, 10) offset = i32(head_data, 10)
# load bitmap information (offset=raster info) # load bitmap information (offset=raster info)
@ -386,7 +393,8 @@ def _save(im, fp, filename, bitmap_header=True):
try: try:
rawmode, bits, colors = SAVE[im.mode] rawmode, bits, colors = SAVE[im.mode]
except KeyError as e: 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 info = im.encoderinfo
@ -414,7 +422,8 @@ def _save(im, fp, filename, bitmap_header=True):
offset = 14 + header + colors * 4 offset = 14 + header + colors * 4
file_size = offset + image file_size = offset + image
if file_size > 2**32 - 1: 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( fp.write(
b"BM" # file type (magic) b"BM" # file type (magic)
+ o32(file_size) # file size + o32(file_size) # file size

View File

@ -42,7 +42,8 @@ class BufrStubImageFile(ImageFile.StubImageFile):
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(4)): 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) self.fp.seek(offset)
@ -60,7 +61,8 @@ class BufrStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename): def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"): 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) _handler.save(im, fp, filename)

View File

@ -40,7 +40,8 @@ def _save(im: Image.Image, fp: BytesIO, filename: str):
h = im.encoderinfo.get("hotspots", [(0, 0) for i in range(len(s))]) h = im.encoderinfo.get("hotspots", [(0, 0) for i in range(len(s))])
if len(h) != len(s): if len(h) != len(s):
raise ValueError("Number of hotspots must be equal to number of cursor sizes") msg = "Number of hotspots must be equal to number of cursor sizes"
raise ValueError(msg)
# sort and remove duplicate sizes # sort and remove duplicate sizes
sizes, hotspots = [], [] sizes, hotspots = [], []
@ -117,7 +118,8 @@ class CurFile(IcoImagePlugin.IcoFile):
# check if CUR # check if CUR
s = buf.read(6) s = buf.read(6)
if not _accept(s): if not _accept(s):
raise SyntaxError("not a CUR file") msg = "not a CUR file"
raise SyntaxError(msg)
self.buf = buf self.buf = buf
self.entry = [] self.entry = []
@ -211,7 +213,8 @@ class CurImageFile(IcoImagePlugin.IcoImageFile):
if len(self.ico.entry) > 0: if len(self.ico.entry) > 0:
self.size = self.ico.entry[0]["dim"] self.size = self.ico.entry[0]["dim"]
else: else:
raise TypeError("No cursors were found") msg = "No cursors were found"
raise TypeError(msg)
self.load() self.load()

View File

@ -47,7 +47,8 @@ class DcxImageFile(PcxImageFile):
# Header # Header
s = self.fp.read(4) s = self.fp.read(4)
if not _accept(s): if not _accept(s):
raise SyntaxError("not a DCX file") msg = "not a DCX file"
raise SyntaxError(msg)
# Component directory # Component directory
self._offset = [] self._offset = []

View File

@ -114,13 +114,16 @@ class DdsImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
if not _accept(self.fp.read(4)): 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("<I", self.fp.read(4)) (header_size,) = struct.unpack("<I", self.fp.read(4))
if header_size != 124: if header_size != 124:
raise OSError(f"Unsupported header size {repr(header_size)}") msg = f"Unsupported header size {repr(header_size)}"
raise OSError(msg)
header_bytes = self.fp.read(header_size - 4) header_bytes = self.fp.read(header_size - 4)
if len(header_bytes) != 120: if len(header_bytes) != 120:
raise OSError(f"Incomplete header: {len(header_bytes)} bytes") msg = f"Incomplete header: {len(header_bytes)} bytes"
raise OSError(msg)
header = BytesIO(header_bytes) header = BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12)) flags, height, width = struct.unpack("<3I", header.read(12))
@ -135,11 +138,19 @@ class DdsImageFile(ImageFile.ImageFile):
fourcc = header.read(4) fourcc = header.read(4)
(bitcount,) = struct.unpack("<I", header.read(4)) (bitcount,) = struct.unpack("<I", header.read(4))
masks = struct.unpack("<4I", header.read(16)) masks = struct.unpack("<4I", header.read(16))
if pfflags & DDPF_RGB: if pfflags & DDPF_LUMINANCE:
# Texture contains uncompressed L or LA data
if pfflags & DDPF_ALPHAPIXELS:
self.mode = "LA"
else:
self.mode = "L"
self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))]
elif pfflags & DDPF_RGB:
# Texture contains uncompressed RGB data # Texture contains uncompressed RGB data
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)} masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
rawmode = "" rawmode = ""
if bitcount == 32: if pfflags & DDPF_ALPHAPIXELS:
rawmode += masks[0xFF000000] rawmode += masks[0xFF000000]
else: else:
self.mode = "RGB" self.mode = "RGB"
@ -208,11 +219,11 @@ class DdsImageFile(ImageFile.ImageFile):
self.info["gamma"] = 1 / 2.2 self.info["gamma"] = 1 / 2.2
return return
else: else:
raise NotImplementedError( msg = f"Unimplemented DXGI format {dxgi_format}"
f"Unimplemented DXGI format {dxgi_format}" raise NotImplementedError(msg)
)
else: else:
raise NotImplementedError(f"Unimplemented pixel format {repr(fourcc)}") msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg)
self.tile = [ self.tile = [
("bcn", (0, 0) + self.size, data_start, (n, self.pixel_format)) ("bcn", (0, 0) + self.size, data_start, (n, self.pixel_format))
@ -223,8 +234,24 @@ class DdsImageFile(ImageFile.ImageFile):
def _save(im, fp, filename): def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA"): if im.mode not in ("RGB", "RGBA", "L", "LA"):
raise OSError(f"cannot write mode {im.mode} as DDS") msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg)
rawmode = im.mode
masks = [0xFF0000, 0xFF00, 0xFF]
if im.mode in ("L", "LA"):
pixel_flags = DDPF_LUMINANCE
else:
pixel_flags = DDPF_RGB
rawmode = rawmode[::-1]
if im.mode in ("LA", "RGBA"):
pixel_flags |= DDPF_ALPHAPIXELS
masks.append(0xFF000000)
bitcount = len(masks) * 8
while len(masks) < 4:
masks.append(0)
fp.write( fp.write(
o32(DDS_MAGIC) o32(DDS_MAGIC)
@ -234,18 +261,15 @@ def _save(im, fp, filename):
) # flags ) # flags
+ o32(im.height) + o32(im.height)
+ o32(im.width) + o32(im.width)
+ o32((im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8) # pitch + o32((im.width * bitcount + 7) // 8) # pitch
+ o32(0) # depth + o32(0) # depth
+ o32(0) # mipmaps + o32(0) # mipmaps
+ o32(0) * 11 # reserved + o32(0) * 11 # reserved
+ o32(32) # pfsize + o32(32) # pfsize
+ o32(DDS_RGBA if im.mode == "RGBA" else DDPF_RGB) # pfflags + o32(pixel_flags) # pfflags
+ o32(0) # fourcc + o32(0) # fourcc
+ o32(32 if im.mode == "RGBA" else 24) # bitcount + o32(bitcount) # bitcount
+ o32(0xFF0000) # rbitmask + b"".join(o32(mask) for mask in masks) # rgbabitmask
+ o32(0xFF00) # gbitmask
+ o32(0xFF) # bbitmask
+ o32(0xFF000000 if im.mode == "RGBA" else 0) # abitmask
+ o32(DDSCAPS_TEXTURE) # dwCaps + o32(DDSCAPS_TEXTURE) # dwCaps
+ o32(0) # dwCaps2 + o32(0) # dwCaps2
+ o32(0) # dwCaps3 + o32(0) # dwCaps3
@ -255,7 +279,7 @@ def _save(im, fp, filename):
if im.mode == "RGBA": if im.mode == "RGBA":
r, g, b, a = im.split() r, g, b, a = im.split()
im = Image.merge("RGBA", (a, r, g, b)) im = Image.merge("RGBA", (a, r, g, b))
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _accept(prefix): def _accept(prefix):

View File

@ -133,7 +133,8 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False):
if gs_windows_binary is not None: if gs_windows_binary is not None:
if not gs_windows_binary: if not gs_windows_binary:
raise OSError("Unable to locate Ghostscript on paths") msg = "Unable to locate Ghostscript on paths"
raise OSError(msg)
command[0] = gs_windows_binary command[0] = gs_windows_binary
# push data through Ghostscript # push data through Ghostscript
@ -229,12 +230,14 @@ class EpsImageFile(ImageFile.ImageFile):
while s_raw: while s_raw:
if s: if s:
if len(s) > 255: if len(s) > 255:
raise SyntaxError("not an EPS file") msg = "not an EPS file"
raise SyntaxError(msg)
try: try:
m = split.match(s) m = split.match(s)
except re.error as e: 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: if m:
k, v = m.group(1, 2) k, v = m.group(1, 2)
@ -268,7 +271,8 @@ class EpsImageFile(ImageFile.ImageFile):
# tools mistakenly put in the Comments section # tools mistakenly put in the Comments section
pass pass
else: else:
raise OSError("bad EPS header") msg = "bad EPS header"
raise OSError(msg)
s_raw = fp.readline() s_raw = fp.readline()
s = s_raw.strip("\r\n") s = s_raw.strip("\r\n")
@ -282,7 +286,8 @@ class EpsImageFile(ImageFile.ImageFile):
while s[:1] == "%": while s[:1] == "%":
if len(s) > 255: if len(s) > 255:
raise SyntaxError("not an EPS file") msg = "not an EPS file"
raise SyntaxError(msg)
if s[:11] == "%ImageData:": if s[:11] == "%ImageData:":
# Encoded bitmapped image. # Encoded bitmapped image.
@ -306,7 +311,8 @@ class EpsImageFile(ImageFile.ImageFile):
break break
if not box: if not box:
raise OSError("cannot determine EPS bounding box") msg = "cannot determine EPS bounding box"
raise OSError(msg)
def _find_offset(self, fp): def _find_offset(self, fp):
@ -326,7 +332,8 @@ class EpsImageFile(ImageFile.ImageFile):
offset = i32(s, 4) offset = i32(s, 4)
length = i32(s, 8) length = i32(s, 8)
else: else:
raise SyntaxError("not an EPS file") msg = "not an EPS file"
raise SyntaxError(msg)
return length, offset return length, offset
@ -365,7 +372,8 @@ def _save(im, fp, filename, eps=1):
elif im.mode == "CMYK": elif im.mode == "CMYK":
operator = (8, 4, b"false 4 colorimage") operator = (8, 4, b"false 4 colorimage")
else: else:
raise ValueError("image mode is not supported") msg = "image mode is not supported"
raise ValueError(msg)
if eps: if eps:
# #

View File

@ -14,318 +14,367 @@ This module provides constants and clear-text names for various
well-known EXIF tags. well-known EXIF tags.
""" """
from enum import IntEnum
TAGS = {
class Base(IntEnum):
# possibly incomplete # possibly incomplete
0x0001: "InteropIndex", InteropIndex = 0x0001
0x000B: "ProcessingSoftware", ProcessingSoftware = 0x000B
0x00FE: "NewSubfileType", NewSubfileType = 0x00FE
0x00FF: "SubfileType", SubfileType = 0x00FF
0x0100: "ImageWidth", ImageWidth = 0x0100
0x0101: "ImageLength", ImageLength = 0x0101
0x0102: "BitsPerSample", BitsPerSample = 0x0102
0x0103: "Compression", Compression = 0x0103
0x0106: "PhotometricInterpretation", PhotometricInterpretation = 0x0106
0x0107: "Thresholding", Thresholding = 0x0107
0x0108: "CellWidth", CellWidth = 0x0108
0x0109: "CellLength", CellLength = 0x0109
0x010A: "FillOrder", FillOrder = 0x010A
0x010D: "DocumentName", DocumentName = 0x010D
0x010E: "ImageDescription", ImageDescription = 0x010E
0x010F: "Make", Make = 0x010F
0x0110: "Model", Model = 0x0110
0x0111: "StripOffsets", StripOffsets = 0x0111
0x0112: "Orientation", Orientation = 0x0112
0x0115: "SamplesPerPixel", SamplesPerPixel = 0x0115
0x0116: "RowsPerStrip", RowsPerStrip = 0x0116
0x0117: "StripByteCounts", StripByteCounts = 0x0117
0x0118: "MinSampleValue", MinSampleValue = 0x0118
0x0119: "MaxSampleValue", MaxSampleValue = 0x0119
0x011A: "XResolution", XResolution = 0x011A
0x011B: "YResolution", YResolution = 0x011B
0x011C: "PlanarConfiguration", PlanarConfiguration = 0x011C
0x011D: "PageName", PageName = 0x011D
0x0120: "FreeOffsets", FreeOffsets = 0x0120
0x0121: "FreeByteCounts", FreeByteCounts = 0x0121
0x0122: "GrayResponseUnit", GrayResponseUnit = 0x0122
0x0123: "GrayResponseCurve", GrayResponseCurve = 0x0123
0x0124: "T4Options", T4Options = 0x0124
0x0125: "T6Options", T6Options = 0x0125
0x0128: "ResolutionUnit", ResolutionUnit = 0x0128
0x0129: "PageNumber", PageNumber = 0x0129
0x012D: "TransferFunction", TransferFunction = 0x012D
0x0131: "Software", Software = 0x0131
0x0132: "DateTime", DateTime = 0x0132
0x013B: "Artist", Artist = 0x013B
0x013C: "HostComputer", HostComputer = 0x013C
0x013D: "Predictor", Predictor = 0x013D
0x013E: "WhitePoint", WhitePoint = 0x013E
0x013F: "PrimaryChromaticities", PrimaryChromaticities = 0x013F
0x0140: "ColorMap", ColorMap = 0x0140
0x0141: "HalftoneHints", HalftoneHints = 0x0141
0x0142: "TileWidth", TileWidth = 0x0142
0x0143: "TileLength", TileLength = 0x0143
0x0144: "TileOffsets", TileOffsets = 0x0144
0x0145: "TileByteCounts", TileByteCounts = 0x0145
0x014A: "SubIFDs", SubIFDs = 0x014A
0x014C: "InkSet", InkSet = 0x014C
0x014D: "InkNames", InkNames = 0x014D
0x014E: "NumberOfInks", NumberOfInks = 0x014E
0x0150: "DotRange", DotRange = 0x0150
0x0151: "TargetPrinter", TargetPrinter = 0x0151
0x0152: "ExtraSamples", ExtraSamples = 0x0152
0x0153: "SampleFormat", SampleFormat = 0x0153
0x0154: "SMinSampleValue", SMinSampleValue = 0x0154
0x0155: "SMaxSampleValue", SMaxSampleValue = 0x0155
0x0156: "TransferRange", TransferRange = 0x0156
0x0157: "ClipPath", ClipPath = 0x0157
0x0158: "XClipPathUnits", XClipPathUnits = 0x0158
0x0159: "YClipPathUnits", YClipPathUnits = 0x0159
0x015A: "Indexed", Indexed = 0x015A
0x015B: "JPEGTables", JPEGTables = 0x015B
0x015F: "OPIProxy", OPIProxy = 0x015F
0x0200: "JPEGProc", JPEGProc = 0x0200
0x0201: "JpegIFOffset", JpegIFOffset = 0x0201
0x0202: "JpegIFByteCount", JpegIFByteCount = 0x0202
0x0203: "JpegRestartInterval", JpegRestartInterval = 0x0203
0x0205: "JpegLosslessPredictors", JpegLosslessPredictors = 0x0205
0x0206: "JpegPointTransforms", JpegPointTransforms = 0x0206
0x0207: "JpegQTables", JpegQTables = 0x0207
0x0208: "JpegDCTables", JpegDCTables = 0x0208
0x0209: "JpegACTables", JpegACTables = 0x0209
0x0211: "YCbCrCoefficients", YCbCrCoefficients = 0x0211
0x0212: "YCbCrSubSampling", YCbCrSubSampling = 0x0212
0x0213: "YCbCrPositioning", YCbCrPositioning = 0x0213
0x0214: "ReferenceBlackWhite", ReferenceBlackWhite = 0x0214
0x02BC: "XMLPacket", XMLPacket = 0x02BC
0x1000: "RelatedImageFileFormat", RelatedImageFileFormat = 0x1000
0x1001: "RelatedImageWidth", RelatedImageWidth = 0x1001
0x1002: "RelatedImageLength", RelatedImageLength = 0x1002
0x4746: "Rating", Rating = 0x4746
0x4749: "RatingPercent", RatingPercent = 0x4749
0x800D: "ImageID", ImageID = 0x800D
0x828D: "CFARepeatPatternDim", CFARepeatPatternDim = 0x828D
0x828E: "CFAPattern", BatteryLevel = 0x828F
0x828F: "BatteryLevel", Copyright = 0x8298
0x8298: "Copyright", ExposureTime = 0x829A
0x829A: "ExposureTime", FNumber = 0x829D
0x829D: "FNumber", IPTCNAA = 0x83BB
0x83BB: "IPTCNAA", ImageResources = 0x8649
0x8649: "ImageResources", ExifOffset = 0x8769
0x8769: "ExifOffset", InterColorProfile = 0x8773
0x8773: "InterColorProfile", ExposureProgram = 0x8822
0x8822: "ExposureProgram", SpectralSensitivity = 0x8824
0x8824: "SpectralSensitivity", GPSInfo = 0x8825
0x8825: "GPSInfo", ISOSpeedRatings = 0x8827
0x8827: "ISOSpeedRatings", OECF = 0x8828
0x8828: "OECF", Interlace = 0x8829
0x8829: "Interlace", TimeZoneOffset = 0x882A
0x882A: "TimeZoneOffset", SelfTimerMode = 0x882B
0x882B: "SelfTimerMode", SensitivityType = 0x8830
0x8830: "SensitivityType", StandardOutputSensitivity = 0x8831
0x8831: "StandardOutputSensitivity", RecommendedExposureIndex = 0x8832
0x8832: "RecommendedExposureIndex", ISOSpeed = 0x8833
0x8833: "ISOSpeed", ISOSpeedLatitudeyyy = 0x8834
0x8834: "ISOSpeedLatitudeyyy", ISOSpeedLatitudezzz = 0x8835
0x8835: "ISOSpeedLatitudezzz", ExifVersion = 0x9000
0x9000: "ExifVersion", DateTimeOriginal = 0x9003
0x9003: "DateTimeOriginal", DateTimeDigitized = 0x9004
0x9004: "DateTimeDigitized", OffsetTime = 0x9010
0x9010: "OffsetTime", OffsetTimeOriginal = 0x9011
0x9011: "OffsetTimeOriginal", OffsetTimeDigitized = 0x9012
0x9012: "OffsetTimeDigitized", ComponentsConfiguration = 0x9101
0x9101: "ComponentsConfiguration", CompressedBitsPerPixel = 0x9102
0x9102: "CompressedBitsPerPixel", ShutterSpeedValue = 0x9201
0x9201: "ShutterSpeedValue", ApertureValue = 0x9202
0x9202: "ApertureValue", BrightnessValue = 0x9203
0x9203: "BrightnessValue", ExposureBiasValue = 0x9204
0x9204: "ExposureBiasValue", MaxApertureValue = 0x9205
0x9205: "MaxApertureValue", SubjectDistance = 0x9206
0x9206: "SubjectDistance", MeteringMode = 0x9207
0x9207: "MeteringMode", LightSource = 0x9208
0x9208: "LightSource", Flash = 0x9209
0x9209: "Flash", FocalLength = 0x920A
0x920A: "FocalLength", Noise = 0x920D
0x920B: "FlashEnergy", 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", 0x920C: "SpatialFrequencyResponse",
0x920D: "Noise",
0x9211: "ImageNumber",
0x9212: "SecurityClassification",
0x9213: "ImageHistory",
0x9214: "SubjectLocation", 0x9214: "SubjectLocation",
0x9215: "ExposureIndex", 0x9215: "ExposureIndex",
0x828E: "CFAPattern",
0x920B: "FlashEnergy",
0x9216: "TIFF/EPStandardID", 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 = { class GPS(IntEnum):
0: "GPSVersionID", GPSVersionID = 0
1: "GPSLatitudeRef", GPSLatitudeRef = 1
2: "GPSLatitude", GPSLatitude = 2
3: "GPSLongitudeRef", GPSLongitudeRef = 3
4: "GPSLongitude", GPSLongitude = 4
5: "GPSAltitudeRef", GPSAltitudeRef = 5
6: "GPSAltitude", GPSAltitude = 6
7: "GPSTimeStamp", GPSTimeStamp = 7
8: "GPSSatellites", GPSSatellites = 8
9: "GPSStatus", GPSStatus = 9
10: "GPSMeasureMode", GPSMeasureMode = 10
11: "GPSDOP", GPSDOP = 11
12: "GPSSpeedRef", GPSSpeedRef = 12
13: "GPSSpeed", GPSSpeed = 13
14: "GPSTrackRef", GPSTrackRef = 14
15: "GPSTrack", GPSTrack = 15
16: "GPSImgDirectionRef", GPSImgDirectionRef = 16
17: "GPSImgDirection", GPSImgDirection = 17
18: "GPSMapDatum", GPSMapDatum = 18
19: "GPSDestLatitudeRef", GPSDestLatitudeRef = 19
20: "GPSDestLatitude", GPSDestLatitude = 20
21: "GPSDestLongitudeRef", GPSDestLongitudeRef = 21
22: "GPSDestLongitude", GPSDestLongitude = 22
23: "GPSDestBearingRef", GPSDestBearingRef = 23
24: "GPSDestBearing", GPSDestBearing = 24
25: "GPSDestDistanceRef", GPSDestDistanceRef = 25
26: "GPSDestDistance", GPSDestDistance = 26
27: "GPSProcessingMethod", GPSProcessingMethod = 27
28: "GPSAreaInformation", GPSAreaInformation = 28
29: "GPSDateStamp", GPSDateStamp = 29
30: "GPSDifferential", GPSDifferential = 30
31: "GPSHPositioningError", GPSHPositioningError = 31
}
"""Maps EXIF GPS tags to tag names.""" """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

View File

@ -28,7 +28,8 @@ class FitsImageFile(ImageFile.ImageFile):
while True: while True:
header = self.fp.read(80) header = self.fp.read(80)
if not header: if not header:
raise OSError("Truncated FITS file") msg = "Truncated FITS file"
raise OSError(msg)
keyword = header[:8].strip() keyword = header[:8].strip()
if keyword == b"END": if keyword == b"END":
break break
@ -36,12 +37,14 @@ class FitsImageFile(ImageFile.ImageFile):
if value.startswith(b"="): if value.startswith(b"="):
value = value[1:].strip() value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"): 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 headers[keyword] = value
naxis = int(headers[b"NAXIS"]) naxis = int(headers[b"NAXIS"])
if naxis == 0: if naxis == 0:
raise ValueError("No image data") msg = "No image data"
raise ValueError(msg)
elif naxis == 1: elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"]) self._size = 1, int(headers[b"NAXIS1"])
else: else:

View File

@ -67,7 +67,8 @@ class FITSStubImageFile(ImageFile.StubImageFile):
def _save(im, fp, filename): def _save(im, fp, filename):
raise OSError("FITS save handler not installed") msg = "FITS save handler not installed"
raise OSError(msg)
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -50,7 +50,8 @@ class FliImageFile(ImageFile.ImageFile):
# HEAD # HEAD
s = self.fp.read(128) s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"): 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 # frames
self.n_frames = i16(s, 6) self.n_frames = i16(s, 6)
@ -141,7 +142,8 @@ class FliImageFile(ImageFile.ImageFile):
self.load() self.load()
if frame != self.__frame + 1: 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 self.__frame = frame
# move to next frame # move to next frame

View File

@ -60,10 +60,12 @@ class FpxImageFile(ImageFile.ImageFile):
try: try:
self.ole = olefile.OleFileIO(self.fp) self.ole = olefile.OleFileIO(self.fp)
except OSError as e: 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": 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) self._open_index(1)
@ -99,7 +101,8 @@ class FpxImageFile(ImageFile.ImageFile):
colors = [] colors = []
bands = i32(s, 4) bands = i32(s, 4)
if bands > 4: if bands > 4:
raise OSError("Invalid number of bands") msg = "Invalid number of bands"
raise OSError(msg)
for i in range(bands): for i in range(bands):
# note: for now, we ignore the "uncalibrated" flag # note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
@ -141,7 +144,8 @@ class FpxImageFile(ImageFile.ImageFile):
length = i32(s, 32) length = i32(s, 32)
if size != self.size: if size != self.size:
raise OSError("subimage mismatch") msg = "subimage mismatch"
raise OSError(msg)
# get tile descriptors # get tile descriptors
fp.seek(28 + offset) fp.seek(28 + offset)
@ -217,7 +221,8 @@ class FpxImageFile(ImageFile.ImageFile):
self.tile_prefix = self.jpeg[jpeg_tables] self.tile_prefix = self.jpeg[jpeg_tables]
else: else:
raise OSError("unknown/invalid compression") msg = "unknown/invalid compression"
raise OSError(msg)
x = x + xtile x = x + xtile
if x >= xsize: if x >= xsize:

View File

@ -73,7 +73,8 @@ def __getattr__(name):
if name in enum.__members__: if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[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): class FtexImageFile(ImageFile.ImageFile):
@ -82,7 +83,8 @@ class FtexImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
raise SyntaxError("not an FTEX file") msg = "not an FTEX file"
raise SyntaxError(msg)
struct.unpack("<i", self.fp.read(4)) # version struct.unpack("<i", self.fp.read(4)) # version
self._size = struct.unpack("<2i", self.fp.read(8)) self._size = struct.unpack("<2i", self.fp.read(8))
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
@ -105,7 +107,8 @@ class FtexImageFile(ImageFile.ImageFile):
elif format == Format.UNCOMPRESSED: elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
else: else:
raise ValueError(f"Invalid texture compression format: {repr(format)}") msg = f"Invalid texture compression format: {repr(format)}"
raise ValueError(msg)
self.fp.close() self.fp.close()
self.fp = BytesIO(data) self.fp = BytesIO(data)

View File

@ -44,18 +44,22 @@ class GbrImageFile(ImageFile.ImageFile):
def _open(self): def _open(self):
header_size = i32(self.fp.read(4)) header_size = i32(self.fp.read(4))
if header_size < 20: if header_size < 20:
raise SyntaxError("not a GIMP brush") msg = "not a GIMP brush"
raise SyntaxError(msg)
version = i32(self.fp.read(4)) version = i32(self.fp.read(4))
if version not in (1, 2): if version not in (1, 2):
raise SyntaxError(f"Unsupported GIMP brush version: {version}") msg = f"Unsupported GIMP brush version: {version}"
raise SyntaxError(msg)
width = i32(self.fp.read(4)) width = i32(self.fp.read(4))
height = i32(self.fp.read(4)) height = i32(self.fp.read(4))
color_depth = i32(self.fp.read(4)) color_depth = i32(self.fp.read(4))
if width <= 0 or height <= 0: if width <= 0 or height <= 0:
raise SyntaxError("not a GIMP brush") msg = "not a GIMP brush"
raise SyntaxError(msg)
if color_depth not in (1, 4): if color_depth not in (1, 4):
raise SyntaxError(f"Unsupported GIMP brush color depth: {color_depth}") msg = f"Unsupported GIMP brush color depth: {color_depth}"
raise SyntaxError(msg)
if version == 1: if version == 1:
comment_length = header_size - 20 comment_length = header_size - 20
@ -63,7 +67,8 @@ class GbrImageFile(ImageFile.ImageFile):
comment_length = header_size - 28 comment_length = header_size - 28
magic_number = self.fp.read(4) magic_number = self.fp.read(4)
if magic_number != b"GIMP": if magic_number != b"GIMP":
raise SyntaxError("not a GIMP brush, bad magic number") msg = "not a GIMP brush, bad magic number"
raise SyntaxError(msg)
self.info["spacing"] = i32(self.fp.read(4)) self.info["spacing"] = i32(self.fp.read(4))
comment = self.fp.read(comment_length)[:-1] comment = self.fp.read(comment_length)[:-1]

View File

@ -49,7 +49,8 @@ class GdImageFile(ImageFile.ImageFile):
s = self.fp.read(1037) s = self.fp.read(1037)
if not i16(s) in [65534, 65535]: if not i16(s) in [65534, 65535]:
raise SyntaxError("Not a valid GD 2.x .gd file") msg = "Not a valid GD 2.x .gd file"
raise SyntaxError(msg)
self.mode = "L" # FIXME: "P" self.mode = "L" # FIXME: "P"
self._size = i16(s, 2), i16(s, 4) self._size = i16(s, 2), i16(s, 4)
@ -87,9 +88,11 @@ def open(fp, mode="r"):
:raises OSError: If the image could not be read. :raises OSError: If the image could not be read.
""" """
if mode != "r": if mode != "r":
raise ValueError("bad mode") msg = "bad mode"
raise ValueError(msg)
try: try:
return GdImageFile(fp) return GdImageFile(fp)
except SyntaxError as e: except SyntaxError as e:
raise UnidentifiedImageError("cannot identify this image file") from e msg = "cannot identify this image file"
raise UnidentifiedImageError(msg) from e

Some files were not shown because too many files have changed in this diff Show More