diff --git a/.appveyor.yml b/.appveyor.yml index 20908052b..b817cd9d8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,7 +10,7 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python310 + - PYTHON: C:/Python311 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python37-x64 diff --git a/.editorconfig b/.editorconfig index d74549fe2..449530717 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,10 @@ indent_style = space trim_trailing_whitespace = true +[*.rst] +# Four-space indentation +indent_size = 4 + [*.yml] # Two-space indentation indent_size = 2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6195f973b..49611e287 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -30,7 +30,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.x" cache: pip cache-dependency-path: "setup.py" diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 65f2b81d5..dfd7d0553 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,7 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype openblas libraqm +brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm PYTHONOPTIMIZE=0 python3 -m pip install cffi python3 -m pip install coverage @@ -13,7 +13,6 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg python3 -m pip install numpy # extra test images diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ffac91cec..8c210bc90 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v6 + uses: actions/stale@v7 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 5b9ab0eda..7b8070d34 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-minor-version: [7, 8, 9] + python-minor-version: [8, 9] timeout-minutes: 40 @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v2 + uses: cygwin/cygwin-install-action@v3 with: platform: x86_64 packages: > @@ -48,7 +48,7 @@ jobs: qt5-devel-tools subversion xorg-server-extra zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v2 + uses: egor-tensin/cleanup-path@v3 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' @@ -76,7 +76,7 @@ jobs: - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" run: | - .ci/build.sh + SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh - name: Test run: | diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index c68d43935..7331cf8ee 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -30,8 +30,8 @@ jobs: centos-stream-9-amd64, debian-10-buster-x86, debian-11-bullseye-x86, - fedora-35-amd64, fedora-36-amd64, + fedora-37-amd64, gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 36bd03e7e..487c3586f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -15,13 +15,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy-3.7" + - python-version: "pypy3.8" architecture: "x64" - - python-version: "pypy-3.8" + - python-version: "pypy3.9" architecture: "x64" timeout-minutes: 30 @@ -65,7 +65,9 @@ jobs: xcopy /S /Y winbuild\depends\test_images\* Tests\images\ # make cache key depend on VS version - & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" | find """catalog_buildVersion""" | ForEach-Object { $a = $_.split(" ")[1]; echo "::set-output name=vs::$a" } + & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` + | find """catalog_buildVersion""" ` + | ForEach-Object { $a = $_.split(" ")[1]; echo "vs=$a" >> $env:GITHUB_OUTPUT } shell: pwsh - name: Cache build @@ -90,19 +92,28 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_zlib.cmd" - - name: Build dependencies / LibTiff + - name: Build dependencies / xz if: steps.build-cache.outputs.cache-hit != 'true' - run: "& winbuild\\build\\build_dep_libtiff.cmd" + run: "& winbuild\\build\\build_dep_xz.cmd" - name: Build dependencies / WebP if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libwebp.cmd" + - name: Build dependencies / LibTiff + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_libtiff.cmd" + # for FreeType CBDT/SBIX font support - name: Build dependencies / libpng if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_libpng.cmd" + # for FreeType WOFF2 font support + - name: Build dependencies / brotli + if: steps.build-cache.outputs.cache-hit != 'true' + run: "& winbuild\\build\\build_dep_brotli.cmd" + - name: Build dependencies / FreeType if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_freetype.cmd" @@ -130,7 +141,7 @@ jobs: if: steps.build-cache.outputs.cache-hit != 'true' run: "& winbuild\\build\\build_dep_fribidi.cmd" - # trim ~150MB x 9 + # trim ~150MB for each job - name: Optimize build cache if: steps.build-cache.outputs.cache-hit != 'true' run: rmdir /S /Q winbuild\build\src @@ -185,16 +196,42 @@ jobs: id: wheel if: "github.event_name != 'pull_request'" run: | - for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a + mkdir fribidi\${{ matrix.architecture }} + copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }} + setlocal EnableDelayedExpansion + for %%f in (winbuild\build\license\*) do ( + set x=%%~nf + rem Skip FriBiDi license, it is not included in the wheel. + set fribidi=!x:~0,7! + if NOT !fribidi!==fribidi ( + rem Skip imagequant license, it is not included in the wheel. + set libimagequant=!x:~0,13! + if NOT !libimagequant!==libimagequant ( + echo. >> LICENSE + echo ===== %%~nf ===== >> LICENSE + echo. >> LICENSE + type %%f >> LICENSE + ) + ) + ) + for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd - - uses: actions/upload-artifact@v3 + - name: Upload wheel + uses: actions/upload-artifact@v3 if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} path: dist\*.whl + - name: Upload fribidi.dll + if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" + uses: actions/upload-artifact@v3 + with: + name: fribidi + path: fribidi\* + success: permissions: contents: none diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c8a1b85f..11c7b77be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: [push, pull_request, workflow_dispatch] permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -20,9 +20,9 @@ jobs: "ubuntu-latest", ] python-version: [ - "pypy-3.8", - "pypy-3.7", - "3.11-dev", + "pypy3.9", + "pypy3.8", + "3.11", "3.10", "3.9", "3.8", @@ -96,7 +96,7 @@ jobs: path: Tests/errors - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11 run: | make doccheck diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml deleted file mode 100644 index 69f9e5476..000000000 --- a/.github/workflows/tidelift.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Tidelift Align - -on: - schedule: - - cron: "30 2 * * *" # daily at 02:30 UTC - push: - paths: - - "Pipfile*" - - ".github/workflows/tidelift.yml" - pull_request: - paths: - - "Pipfile*" - - ".github/workflows/tidelift.yml" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - if: github.repository_owner == 'python-pillow' - name: Run Tidelift to ensure approved open source packages are in use - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Scan - uses: tidelift/alignment-action@main - env: - TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }} - TIDELIFT_ORGANIZATION: team/aclark4life - TIDELIFT_PROJECT: pillow diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f81bcb956..d019d3e7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,25 @@ repos: - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.12.0 hooks: - id: black - args: ["--target-version", "py37"] + args: [--target-version=py37] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.11.1 hooks: - id: isort + - repo: https://github.com/PyCQA/bandit + rev: 1.7.4 + hooks: + - id: bandit + args: [--severity-level=high] + files: ^src/ + - repo: https://github.com/asottile/yesqa rev: v1.4.0 hooks: @@ -25,10 +32,11 @@ repos: exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 - additional_dependencies: [flake8-2020, flake8-implicit-str-concat] + additional_dependencies: + [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.9.0 @@ -37,16 +45,21 @@ repos: - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-merge-conflict - id: check-json - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v0.6.1 + rev: v0.6.7 hooks: - id: sphinx-lint + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: 0.5.2 + hooks: + - id: tox-ini-fmt + ci: autoupdate_schedule: monthly diff --git a/CHANGES.rst b/CHANGES.rst index 060569ab5..904c73629 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,138 @@ Changelog (Pillow) ================== -9.3.0 (unreleased) +9.4.0 (unreleased) ------------------ +- Improve exception traceback readability #6836 + [hugovk, radarhere] + +- Do not attempt to read IFD1 if absent #6840 + [radarhere] + +- Fixed writing int as ASCII tag #6800 + [radarhere] + +- If available, use wl-paste or xclip for grabclipboard() on Linux #6783 + [radarhere] + +- Added signed option when saving JPEG2000 images #6709 + [radarhere] + +- Patch OpenJPEG to include ARM64 fix #6718 + [radarhere] + +- Added support for I;16 modes in putdata() #6825 + [radarhere] + +- Added conversion from RGBa to RGB #6708 + [radarhere] + +- Added DDS support for uncompressed L and LA images #6820 + [radarhere, REDxEYE] + +- Added LightSource tag values to ExifTags #6749 + [radarhere] + +- Fixed PyAccess after changing ICO size #6821 + [radarhere] + +- Do not use EXIF from info when saving PNG images #6819 + [radarhere] + +- Fixed saving EXIF data to MPO #6817 + [radarhere] + +- Added Exif hide_offsets() #6762 + [radarhere] + +- Only compare to previous frame when checking for duplicate GIF frames while saving #6787 + [radarhere] + +- Always initialize all plugins in registered_extensions() #6811 + [radarhere] + +- Ignore non-opaque WebP background when saving as GIF #6792 + [radarhere] + +- Only set tile in ImageFile __setstate__ #6793 + [radarhere] + +- When reading BLP, do not trust JPEG decoder to determine image is CMYK #6767 + [radarhere] + +- Added IFD enum to ExifTags #6748 + [radarhere] + +- Fixed bug combining GIF frame durations #6779 + [radarhere] + +- Support saving JPEG comments #6774 + [smason, radarhere] + +- Added getxmp() to WebPImagePlugin #6758 + [radarhere] + +- Added "exact" option when saving WebP #6747 + [ashafaei, radarhere] + +- Use fractional coordinates when drawing text #6722 + [radarhere] + +- Fixed writing int as BYTE tag #6740 + [radarhere] + +- Added MP Format Version when saving MPO #6735 + [radarhere] + +- Added Interop to ExifTags #6724 + [radarhere] + +- CVE-2007-4559 patch when building on Windows #6704 + [TrellixVulnTeam, nulano, radarhere] + +- Fix compiler warning: accessing 64 bytes in a region of size 48 #6714 + [wiredfool] + +- Use verbose flag for pip install #6713 + [wiredfool, radarhere] + +9.3.0 (2022-10-29) +------------------ + +- Limit SAMPLESPERPIXEL to avoid runtime DOS #6700 + [wiredfool] + +- Initialize libtiff buffer when saving #6699 + [radarhere] + +- Inline fname2char to fix memory leak #6329 + [nulano] + +- Fix memory leaks related to text features #6330 + [nulano] + +- Use double quotes for version check on old CPython on Windows #6695 + [hugovk] + +- Remove backup implementation of Round for Windows platforms #6693 + [cgohlke] + +- Fixed set_variation_by_name offset #6445 + [radarhere] + +- Fix malloc in _imagingft.c:font_setvaraxes #6690 + [cgohlke] + +- Release Python GIL when converting images using matrix operations #6418 + [hmaarrfk] + +- Added ExifTags enums #6630 + [radarhere] + +- Do not modify previous frame when calculating delta in PNG #6683 + [radarhere] + - Added support for reading BMP images with RLE4 compression #6674 [npjg, radarhere] diff --git a/MANIFEST.in b/MANIFEST.in index 08f6dfc08..f51551303 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include *.c include *.h include *.in -include *.lock include *.md include *.py include *.rst @@ -10,7 +9,6 @@ include *.txt include *.yaml include LICENSE include Makefile -include Pipfile include tox.ini graft Tests graft src diff --git a/Makefile b/Makefile index 8f2862948..a2545b54e 100644 --- a/Makefile +++ b/Makefile @@ -53,12 +53,12 @@ inplace: clean .PHONY: install install: - python3 -m pip install . + python3 -m pip -v install . python3 selftest.py .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" . + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" . python3 selftest.py .PHONY: debug @@ -67,7 +67,7 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null .PHONY: release-test release-test: diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 1e611a63c..000000000 --- a/Pipfile +++ /dev/null @@ -1,22 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -black = "*" -check-manifest = "*" -coverage = "*" -defusedxml = "*" -packaging = "*" -markdown2 = "*" -olefile = "*" -pyroma = "*" -pytest = "*" -pytest-cov = "*" -pytest-timeout = "*" - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 600b19050..000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,324 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "attrs": { - "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" - }, - "black": { - "hashes": [ - "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", - "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" - ], - "index": "pypi", - "version": "==21.12b0" - }, - "build": { - "hashes": [ - "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", - "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "charset-normalizer": { - "hashes": [ - "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", - "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" - ], - "markers": "python_version >= '3'", - "version": "==2.0.9" - }, - "check-manifest": { - "hashes": [ - "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95", - "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce" - ], - "index": "pypi", - "version": "==0.47" - }, - "click": { - "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" - ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" - }, - "coverage": { - "hashes": [ - "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", - "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", - "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", - "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", - "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", - "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", - "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", - "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", - "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", - "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", - "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", - "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", - "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", - "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", - "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", - "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", - "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", - "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", - "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", - "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", - "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", - "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", - "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", - "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", - "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", - "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", - "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", - "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", - "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", - "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", - "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", - "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", - "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", - "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", - "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", - "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", - "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", - "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", - "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", - "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", - "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", - "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", - "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", - "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", - "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", - "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", - "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" - ], - "index": "pypi", - "version": "==6.2" - }, - "defusedxml": { - "hashes": [ - "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", - "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" - ], - "index": "pypi", - "version": "==0.7.1" - }, - "docutils": { - "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.18.1" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3'", - "version": "==3.3" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "markdown2": { - "hashes": [ - "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0", - "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e" - ], - "index": "pypi", - "version": "==2.4.2" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "olefile": { - "hashes": [ - "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" - ], - "index": "pypi", - "version": "==0.46" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "index": "pypi", - "version": "==21.3" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "version": "==0.9.0" - }, - "pep517": { - "hashes": [ - "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", - "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" - ], - "version": "==0.12.0" - }, - "platformdirs": { - "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" - ], - "markers": "python_version >= '3.6'", - "version": "==2.4.0" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pygments": { - "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" - ], - "markers": "python_version >= '3.5'", - "version": "==2.10.0" - }, - "pyparsing": { - "hashes": [ - "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", - "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.6" - }, - "pyroma": { - "hashes": [ - "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65", - "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a" - ], - "index": "pypi", - "version": "==3.2" - }, - "pytest": { - "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" - ], - "index": "pypi", - "version": "==6.2.5" - }, - "pytest-cov": { - "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" - ], - "index": "pypi", - "version": "==3.0.0" - }, - "pytest-timeout": { - "hashes": [ - "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112", - "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717" - ], - "index": "pypi", - "version": "==2.0.2" - }, - "requests": { - "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.26.0" - }, - "setuptools": { - "hashes": [ - "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c", - "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b" - ], - "markers": "python_version >= '3.7'", - "version": "==60.0.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", - "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" - ], - "markers": "python_version >= '3.6'", - "version": "==1.2.3" - }, - "typing-extensions": { - "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" - } - }, - "develop": {} -} diff --git a/README.md b/README.md index e7c0ebc5a..8ee68f9b8 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ As of 2019, Pillow development is Code coverage - Tidelift Align + Fuzzing Status diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index 104ff677c..da559b3d3 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -8,6 +8,7 @@ TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa ter-x20b.pcf, from http://terminus-font.sourceforge.net/ BungeeColor-Regular_colr_Windows.ttf, from https://github.com/djrrb/bungee +OpenSans.woff2, from https://fonts.googleapis.com/css?family=Open+Sans All of the above fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. diff --git a/Tests/fonts/OpenSans.woff2 b/Tests/fonts/OpenSans.woff2 new file mode 100644 index 000000000..15339ea9c Binary files /dev/null and b/Tests/fonts/OpenSans.woff2 differ diff --git a/Tests/images/duplicate_frame.gif b/Tests/images/duplicate_frame.gif new file mode 100644 index 000000000..ef0c894a5 Binary files /dev/null and b/Tests/images/duplicate_frame.gif differ diff --git a/Tests/images/flower_thumbnail.png b/Tests/images/flower_thumbnail.png new file mode 100644 index 000000000..4a362535f Binary files /dev/null and b/Tests/images/flower_thumbnail.png differ diff --git a/Tests/images/hopper_lzma.tif b/Tests/images/hopper_lzma.tif new file mode 100644 index 000000000..d7ca089fc Binary files /dev/null and b/Tests/images/hopper_lzma.tif differ diff --git a/Tests/images/hopper_webp.png b/Tests/images/hopper_webp.png new file mode 100644 index 000000000..94b927ac2 Binary files /dev/null and b/Tests/images/hopper_webp.png differ diff --git a/Tests/images/hopper_webp.tif b/Tests/images/hopper_webp.tif new file mode 100644 index 000000000..5e398606c Binary files /dev/null and b/Tests/images/hopper_webp.tif differ diff --git a/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif new file mode 100644 index 000000000..01dca594f Binary files /dev/null and b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif differ diff --git a/Tests/images/test_anchor_multiline_mm_right.png b/Tests/images/test_anchor_multiline_mm_right.png index cf002b12c..7e98b8eac 100644 Binary files a/Tests/images/test_anchor_multiline_mm_right.png and b/Tests/images/test_anchor_multiline_mm_right.png differ diff --git a/Tests/images/test_combine_multiline_lm_center.png b/Tests/images/test_combine_multiline_lm_center.png index 7b1e9c4e4..6a1513024 100644 Binary files a/Tests/images/test_combine_multiline_lm_center.png and b/Tests/images/test_combine_multiline_lm_center.png differ diff --git a/Tests/images/test_combine_multiline_lm_left.png b/Tests/images/test_combine_multiline_lm_left.png index a26996c2d..8eb254fdf 100644 Binary files a/Tests/images/test_combine_multiline_lm_left.png and b/Tests/images/test_combine_multiline_lm_left.png differ diff --git a/Tests/images/test_combine_multiline_lm_right.png b/Tests/images/test_combine_multiline_lm_right.png index 7caf5cb74..cb640a740 100644 Binary files a/Tests/images/test_combine_multiline_lm_right.png and b/Tests/images/test_combine_multiline_lm_right.png differ diff --git a/Tests/images/test_combine_multiline_mm_center.png b/Tests/images/test_combine_multiline_mm_center.png index a859e9570..d1146b8b8 100644 Binary files a/Tests/images/test_combine_multiline_mm_center.png and b/Tests/images/test_combine_multiline_mm_center.png differ diff --git a/Tests/images/test_combine_multiline_mm_left.png b/Tests/images/test_combine_multiline_mm_left.png index aadb5191f..f539a8e62 100644 Binary files a/Tests/images/test_combine_multiline_mm_left.png and b/Tests/images/test_combine_multiline_mm_left.png differ diff --git a/Tests/images/test_combine_multiline_mm_right.png b/Tests/images/test_combine_multiline_mm_right.png index 8238d4ec8..02634163e 100644 Binary files a/Tests/images/test_combine_multiline_mm_right.png and b/Tests/images/test_combine_multiline_mm_right.png differ diff --git a/Tests/images/test_combine_multiline_rm_center.png b/Tests/images/test_combine_multiline_rm_center.png index 7568dd63a..4cce8f6a0 100644 Binary files a/Tests/images/test_combine_multiline_rm_center.png and b/Tests/images/test_combine_multiline_rm_center.png differ diff --git a/Tests/images/test_combine_multiline_rm_left.png b/Tests/images/test_combine_multiline_rm_left.png index b8c3b5b14..93d8162b3 100644 Binary files a/Tests/images/test_combine_multiline_rm_left.png and b/Tests/images/test_combine_multiline_rm_left.png differ diff --git a/Tests/images/test_combine_multiline_rm_right.png b/Tests/images/test_combine_multiline_rm_right.png index 14c478a72..6c4634560 100644 Binary files a/Tests/images/test_combine_multiline_rm_right.png and b/Tests/images/test_combine_multiline_rm_right.png differ diff --git a/Tests/images/test_woff2.png b/Tests/images/test_woff2.png new file mode 100644 index 000000000..4eb3be4c7 Binary files /dev/null and b/Tests/images/test_woff2.png differ diff --git a/Tests/images/text_float_coord.png b/Tests/images/text_float_coord.png index 49468698c..d2270826a 100644 Binary files a/Tests/images/text_float_coord.png and b/Tests/images/text_float_coord.png differ diff --git a/Tests/images/text_float_coord_1_alt.png b/Tests/images/text_float_coord_1_alt.png index 50bdac3d8..2287071ff 100644 Binary files a/Tests/images/text_float_coord_1_alt.png and b/Tests/images/text_float_coord_1_alt.png differ diff --git a/Tests/images/uncompressed_l.dds b/Tests/images/uncompressed_l.dds new file mode 100644 index 000000000..b82282587 Binary files /dev/null and b/Tests/images/uncompressed_l.dds differ diff --git a/Tests/images/uncompressed_l.png b/Tests/images/uncompressed_l.png new file mode 100644 index 000000000..9d22a26a4 Binary files /dev/null and b/Tests/images/uncompressed_l.png differ diff --git a/Tests/images/uncompressed_la.dds b/Tests/images/uncompressed_la.dds new file mode 100644 index 000000000..30bf93576 Binary files /dev/null and b/Tests/images/uncompressed_la.dds differ diff --git a/Tests/images/uncompressed_la.png b/Tests/images/uncompressed_la.png new file mode 100644 index 000000000..0d4ea602f Binary files /dev/null and b/Tests/images/uncompressed_la.png differ diff --git a/Tests/images/variation_adobe_name.png b/Tests/images/variation_adobe_name.png index 11ceaf6e6..5168e04b9 100644 Binary files a/Tests/images/variation_adobe_name.png and b/Tests/images/variation_adobe_name.png differ diff --git a/Tests/images/variation_adobe_older_harfbuzz_name.png b/Tests/images/variation_adobe_older_harfbuzz_name.png index 2adb517a7..fa0e307b4 100644 Binary files a/Tests/images/variation_adobe_older_harfbuzz_name.png and b/Tests/images/variation_adobe_older_harfbuzz_name.png differ diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 09cc7bc16..7e9098f53 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -19,29 +19,17 @@ python3 setup.py build --build-base=/tmp/build install # Build fuzzers in $OUT. for fuzzer in $(find $SRC -name 'fuzz_*.py'); do - fuzzer_basename=$(basename -s .py $fuzzer) - fuzzer_package=${fuzzer_basename}.pkg - pyinstaller \ + compile_python_fuzzer $fuzzer \ --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \ --add-binary /usr/local/lib/libfreetype.so.6:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \ --add-binary /usr/local/lib/libpng16.so.16:. \ - --add-binary /usr/local/lib/libtiff.so.5:. \ + --add-binary /usr/local/lib/libtiff.so.6:. \ --add-binary /usr/local/lib/libwebp.so.7:. \ --add-binary /usr/local/lib/libwebpdemux.so.2:. \ --add-binary /usr/local/lib/libwebpmux.so.3:. \ - --add-binary /usr/local/lib/libxcb.so.1:. \ - --distpath $OUT --onefile --name $fuzzer_package $fuzzer - - # Create execution wrapper. - echo "#!/bin/sh -# LLVMFuzzerTestOneInput for fuzzer detection. -this_dir=\$(dirname \"\$0\") -LD_PRELOAD=\$this_dir/sanitizer_with_fuzzer.so \ -ASAN_OPTIONS=\$ASAN_OPTIONS:symbolize=1:external_symbolizer_path=\$this_dir/llvm-symbolizer:detect_leaks=0 \ -\$this_dir/$fuzzer_package \$@" > $OUT/$fuzzer_basename - chmod u+x $OUT/$fuzzer_basename + --add-binary /usr/local/lib/libxcb.so.1:. done find Tests/images Tests/icc -print | zip -q $OUT/fuzz_pillow_seed_corpus.zip -@ diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index b17aad2ea..ed9aff9cc 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -35,6 +35,7 @@ def test_questionable(): "pal8os2v2.bmp", "rgb24prof.bmp", "pal1p1.bmp", + "pal4rletrns.bmp", "pal8offs.bmp", "rgb24lprof.bmp", "rgb32fakealpha.bmp", diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 1f5567163..51637c786 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -553,18 +553,20 @@ def test_apng_save_disposal(tmp_path): def test_apng_save_disposal_previous(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) - transparent = Image.new("RGBA", size, (0, 0, 0, 0)) + blue = Image.new("RGBA", size, (0, 0, 255, 255)) red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) # test OP_NONE - transparent.save( + blue.save( test_file, save_all=True, append_images=[red, green], disposal=PngImagePlugin.Disposal.OP_PREVIOUS, ) with Image.open(test_file) as im: + assert im.getpixel((0, 0)) == (0, 0, 255, 255) + im.seek(2) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 4b9f8949e..cac4108a8 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -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_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" +TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" +TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" @@ -194,26 +196,24 @@ def test_unimplemented_dxgi_format(): pass -def test_uncompressed_rgb(): - """Check uncompressed RGB images can be opened""" +@pytest.mark.parametrize( + ("mode", "size", "test_file"), + [ + ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), + ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), + ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), + ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), + ], +) +def test_uncompressed(mode, size, test_file): + """Check uncompressed images can be opened""" - # convert -format dds -define dds:compression=none hopper.jpg hopper.dds - with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: + with Image.open(test_file) as im: assert im.format == "DDS" - assert im.mode == "RGB" - assert im.size == (128, 128) + assert im.mode == mode + assert im.size == size - assert_image_equal_tofile(im, "Tests/images/hopper.png") - - # Test image with alpha - with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im: - assert im.format == "DDS" - assert im.mode == "RGBA" - assert im.size == (800, 600) - - assert_image_equal_tofile( - im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png") - ) + assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) def test__accept_true(): @@ -305,6 +305,8 @@ def test_save_unsupported_mode(tmp_path): @pytest.mark.parametrize( ("mode", "test_file"), [ + ("L", "Tests/images/linear_gradient.png"), + ("LA", "Tests/images/uncompressed_la.png"), ("RGB", "Tests/images/hopper.png"), ("RGBA", "Tests/images/pil123rgba.png"), ], diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 926f5c1ee..d48fc1442 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -677,6 +677,24 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == (255, 0, 0) +def test_dispose2_background_frame(tmp_path): + out = str(tmp_path / "temp.gif") + + im_list = [Image.new("RGBA", (1, 20))] + + different_frame = Image.new("RGBA", (1, 20)) + different_frame.putpixel((0, 10), (255, 0, 0, 255)) + im_list.append(different_frame) + + # Frame that matches the background + im_list.append(Image.new("RGBA", (1, 20))) + + im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) + + with Image.open(out) as im: + assert im.n_frames == 3 + + def test_transparency_in_second_frame(tmp_path): out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: @@ -791,6 +809,22 @@ def test_roundtrip_info_duration(tmp_path): ] == duration_list +def test_roundtrip_info_duration_combined(tmp_path): + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/duplicate_frame.gif") as im: + assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ + 1000, + 1000, + 1000, + ] + im.save(out, save_all=True) + + with Image.open(out) as reloaded: + assert [ + frame.info["duration"] for frame in ImageSequence.Iterator(reloaded) + ] == [1000, 2000] + + def test_identical_frames(tmp_path): duration_list = [1000, 1500, 2000, 4000] @@ -859,14 +893,23 @@ def test_background(tmp_path): im.info["background"] = 1 im.save(out) with Image.open(out) as reread: - assert reread.info["background"] == im.info["background"] + +def test_webp_background(tmp_path): + out = str(tmp_path / "temp.gif") + + # Test opaque WebP background if features.check("webp") and features.check("webp_anim"): with Image.open("Tests/images/hopper.webp") as im: - assert isinstance(im.info["background"], tuple) + assert im.info["background"] == (255, 255, 255, 255) im.save(out) + # Test non-opaque WebP background + im = Image.new("L", (100, 100), "#000") + im.info["background"] = (0, 0, 0, 0) + im.save(out) + def test_comment(tmp_path): with Image.open(TEST_GIF) as im: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 3fcd5c61f..afb17b1af 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -71,6 +71,19 @@ def test_save_to_bytes(): ) +def test_getpixel(tmp_path): + temp_file = str(tmp_path / "temp.ico") + + im = hopper() + im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) + + with Image.open(temp_file) as reloaded: + reloaded.load() + reloaded.size = (32, 32) + + assert reloaded.getpixel((0, 0)) == (18, 20, 62) + + def test_no_duplicates(tmp_path): temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index fa96e425b..eabc6bf75 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -86,6 +86,33 @@ class TestFileJpeg: assert len(im.applist) == 2 assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + assert im.app["COM"] == im.info["comment"] + + def test_comment_write(self): + with Image.open(TEST_FILE) as im: + assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" + + # Test that existing comment is saved by default + out = BytesIO() + im.save(out, format="JPEG") + with Image.open(out) as reloaded: + assert im.info["comment"] == reloaded.info["comment"] + + # Ensure that a blank comment causes any existing comment to be removed + for comment in ("", b"", None): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + assert "comment" not in reloaded.info + + # Test that a comment argument overrides the default comment + for comment in ("Test comment text", b"Text comment text"): + out = BytesIO() + im.save(out, format="JPEG", comment=comment) + with Image.open(out) as reloaded: + if not isinstance(comment, bytes): + comment = comment.encode() + assert reloaded.info["comment"] == comment def test_cmyk(self): # Test CMYK handling. Thanks to Tim and Charlie for test data, @@ -415,6 +442,13 @@ class TestFileJpeg: info = im._getexif() assert info[305] == "Adobe Photoshop CS Macintosh" + def test_get_child_images(self): + with Image.open("Tests/images/flower.jpg") as im: + ims = im.get_child_images() + + assert len(ims) == 1 + assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index cd142e67f..0229b2243 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -252,6 +252,20 @@ def test_mct(): assert_image_similar(im, jp2, 1.0e-3) +def test_sgnd(tmp_path): + outfile = str(tmp_path / "temp.jp2") + + im = Image.new("L", (1, 1)) + im.save(outfile) + with Image.open(outfile) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + im = Image.new("L", (1, 1)) + im.save(outfile, signed=True) + with Image.open(outfile) as reloaded_signed: + assert reloaded_signed.getpixel((0, 0)) == 128 + + def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 58d3aac06..1109cd15e 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -3,6 +3,7 @@ import io import itertools import os import re +import sys from collections import namedtuple import pytest @@ -825,6 +826,44 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.mode == "F" assert reloaded.getexif()[SAMPLEFORMAT] == 3 + def test_lzma(self, capfd): + try: + with Image.open("Tests/images/hopper_lzma.tif") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + im2 = hopper() + assert_image_similar(im, im2, 5) + except OSError: + captured = capfd.readouterr() + if "LZMA compression support is not configured" in captured.err: + pytest.skip("LZMA compression support is not configured") + sys.stdout.write(captured.out) + sys.stderr.write(captured.err) + raise + + def test_webp(self, capfd): + try: + with Image.open("Tests/images/hopper_webp.tif") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "TIFF" + assert_image_similar_tofile(im, "Tests/images/hopper_webp.png", 1) + except OSError: + captured = capfd.readouterr() + if "WEBP compression support is not configured" in captured.err: + pytest.skip("WEBP compression support is not configured") + if ( + "Compression scheme 50001 strip decoding is not implemented" + in captured.err + ): + pytest.skip( + "Compression scheme 50001 strip decoding is not implemented" + ) + sys.stdout.write(captured.out) + sys.stderr.write(captured.err) + raise + def test_lzw(self): with Image.open("Tests/images/hopper_lzw.tif") as im: assert im.mode == "RGB" diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index d94bdaa96..3e5476222 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -80,7 +80,10 @@ def test_app(test_file): @pytest.mark.parametrize("test_file", test_files) def test_exif(test_file): - with Image.open(test_file) as im: + with Image.open(test_file) as im_original: + im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) + + for im in (im_original, im_reloaded): info = im._getexif() assert info[272] == "Nintendo 3DS" assert info[296] == 2 @@ -268,6 +271,7 @@ def test_save_all(): im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) assert_image_equal(im, im_reloaded) + assert im_reloaded.mpinfo[45056] == b"0100" im_reloaded.seek(1) assert_image_similar(im2, im_reloaded, 1) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 4129e8783..9667b6a4a 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -42,7 +42,6 @@ def test_save(tmp_path, mode): helper_save_as_pdf(tmp_path, mode) -@pytest.mark.valgrind_known_error(reason="Temporary skip") def test_monochrome(tmp_path): # Arrange mode = "1" diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 37235fe6f..9481cd5dd 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -706,10 +706,18 @@ class TestFilePng: assert exif[274] == 3 def test_exif_save(self, tmp_path): + # Test exif is not saved from info + test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/exif.png") as im: - test_file = str(tmp_path / "temp.png") im.save(test_file) + with Image.open(test_file) as reloaded: + assert reloaded._getexif() is None + + # Test passing in exif + with Image.open("Tests/images/exif.png") as im: + im.save(test_file, exif=im.getexif()) + with Image.open(test_file) as reloaded: exif = reloaded._getexif() assert exif[274] == 1 @@ -720,7 +728,7 @@ class TestFilePng: def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") - im.save(test_file) + im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: exif = reloaded._getexif() diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a32e6a005..4f3c8e390 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -4,7 +4,7 @@ from io import BytesIO import pytest -from PIL import Image, ImageFile, TiffImagePlugin +from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -858,6 +858,19 @@ class TestFileTiff: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", + ], + ) + @pytest.mark.timeout(2) + def test_oom(self, test_file): + with pytest.raises(UnidentifiedImageError): + with pytest.warns(UserWarning): + with Image.open(test_file): + pass + @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index d38c1c523..48797ea08 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,20 +185,37 @@ def test_iptc(tmp_path): im.save(out) -def test_writing_bytes_to_ascii(tmp_path): - im = hopper() +@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) +def test_writing_other_types_to_ascii(value, expected, tmp_path): info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] assert tag.type == TiffTags.ASCII - info[271] = b"test" + info[271] = value + + im = hopper() + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[271] == expected + + +def test_writing_int_to_bytes(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[700] + assert tag.type == TiffTags.BYTE + + info[700] = 1 out = str(tmp_path / "temp.tiff") im.save(out, tiffinfo=info) with Image.open(out) as reloaded: - assert reloaded.tag_v2[271] == "test" + assert reloaded.tag_v2[700] == b"\x01" def test_undefined_zero(tmp_path): diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index dc82fb742..5970fd2a3 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -97,6 +97,35 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) +def test_keep_rgb_values_when_transparent(tmp_path): + """ + Saving transparent pixels should retain their original RGB values + when using the "exact" parameter. + """ + + image = hopper("RGB") + + # create a copy of the image + # with the left half transparent + half_transparent_image = image.copy() + new_alpha = Image.new("L", (128, 128), 255) + new_alpha.paste(0, (0, 0, 64, 128)) + half_transparent_image.putalpha(new_alpha) + + # save with transparent area preserved + temp_file = str(tmp_path / "temp.webp") + half_transparent_image.save(temp_file, exact=True, lossless=True) + + with Image.open(temp_file) as reloaded: + assert reloaded.mode == "RGBA" + assert reloaded.format == "WEBP" + + # even though it is lossless, if we don't use exact=True + # in libwebp >= 0.5, the transparent area will be filled with black + # (or something more conducive to compression) + assert_image_equal(reloaded.convert("RGB"), image) + + def test_write_unsupported_mode_PA(tmp_path): """ Saving a palette-based file with transparency to WebP format diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index f77a245c0..4f513d82b 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -11,6 +11,11 @@ pytestmark = [ skip_unless_feature("webp_mux"), ] +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + def test_read_exif_metadata(): @@ -110,6 +115,22 @@ def test_read_no_exif(): assert not webp_image._getexif() +def test_getxmp(): + with Image.open("Tests/images/flower.webp") as im: + assert "xmp" not in im.info + assert im.getxmp() == {} + + with Image.open("Tests/images/flower2.webp") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + assert ( + im.getxmp()["xmpmeta"]["xmptk"] + == "Adobe XMP Core 5.3-c011 66.145661, 2012/02/06-14:56:27 " + ) + + @skip_unless_feature("webp_anim") def test_write_animated_metadata(tmp_path): iccp_data = b"" diff --git a/Tests/test_image.py b/Tests/test_image.py index e57903490..a37c90296 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,7 +7,14 @@ import warnings import pytest -from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features +from PIL import ( + ExifTags, + Image, + ImageDraw, + ImagePalette, + UnidentifiedImageError, + features, +) from .helper import ( assert_image_equal, @@ -394,8 +401,6 @@ class TestImage: def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 - extension = Image.EXTENSION - Image.EXTENSION = {} # Act Image.registered_extensions() @@ -403,10 +408,6 @@ class TestImage: # Assert assert Image._initialized == 2 - # Restore the original state and assert - Image.EXTENSION = extension - assert Image.EXTENSION - def test_registered_extensions(self): # Arrange # Open an image to trigger plugin registration @@ -808,6 +809,18 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) + def test_exif_ifd1(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + assert exif.get_ifd(ExifTags.IFD.IFD1) == { + 513: 2036, + 514: 5448, + 259: 6, + 296: 2, + 282: 180.0, + 283: 180.0, + } + def test_exif_ifd(self): with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -838,6 +851,31 @@ class TestImage: 34665: 196, } + def test_exif_hide_offsets(self): + with Image.open("Tests/images/flower.jpg") as im: + exif = im.getexif() + + # Check offsets are present initially + assert 0x8769 in exif + for tag in (0xA005, 0x927C): + assert tag in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + loaded_exif = exif + + with Image.open("Tests/images/flower.jpg") as im: + new_exif = im.getexif() + + for exif in (loaded_exif, new_exif): + exif.hide_offsets() + + # Assert they are hidden afterwards, + # but that the IFDs are still available + assert 0x8769 not in exif + assert exif.get_ifd(0x8769) + for tag in (0xA005, 0x927C): + assert tag not in exif.get_ifd(0x8769) + assert exif.get_ifd(0xA005) + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) def test_zero_tobytes(self, size): im = Image.new("RGB", size) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 955740b95..6c4f1ceec 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -4,11 +4,10 @@ import sys import sysconfig import pytest -from setuptools.command.build_ext import new_compiler from PIL import Image -from .helper import assert_image_equal, hopper, is_win32, on_ci +from .helper import assert_image_equal, hopper, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -406,14 +405,13 @@ class TestImagePutPixelError(AccessTest): class TestEmbeddable: - @pytest.mark.skipif( - not is_win32() or on_ci(), - reason="Failing on AppVeyor / GitHub Actions when run from subprocess, " - "not from shell", - ) + @pytest.mark.xfail(reason="failing test") + @pytest.mark.skipif(not is_win32(), reason="requires Windows") def test_embeddable(self): import ctypes + from setuptools.command.build_ext import new_compiler + with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( """ diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 902d8bf8f..0a7202a33 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -104,6 +104,13 @@ def test_rgba_p(): assert_image_similar(im, comparable, 20) +def test_rgba(): + with Image.open("Tests/images/transparent.png") as im: + assert im.mode == "RGBA" + + assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) + + def test_trns_p(tmp_path): im = hopper("P") im.info["transparency"] = 0 diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 3d60e52a2..0e6293349 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -55,10 +55,11 @@ def test_mode_with_L_with_float(): assert im.getpixel((0, 0)) == 2 -def test_mode_i(): +@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) +def test_mode_i(mode): src = hopper("L") data = list(src.getdata()) - im = Image.new("I", src.size, 0) + im = Image.new(mode, src.size, 0) im.putdata(data, 2, 256) target = [2 * elt + 256 for elt in data] diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 76b7c65cc..4c4c41b7b 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1238,6 +1238,27 @@ def test_stroke_descender(): assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) +@skip_unless_feature("freetype2") +def test_split_word(): + # Arrange + im = Image.new("RGB", (230, 55)) + expected = im.copy() + expected_draw = ImageDraw.Draw(expected) + font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 48) + expected_draw.text((0, 0), "paradise", font=font) + + draw = ImageDraw.Draw(im) + + # Act + draw.text((0, 0), "par", font=font) + + length = draw.textlength("par", font=font) + draw.text((length, 0), "adise", font=font) + + # Assert + assert_image_equal(im, expected) + + @skip_unless_feature("freetype2") def test_stroke_multiline(): # Arrange diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index e5cf8b631..306a2f1bf 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -746,12 +746,14 @@ def test_variation_set_by_name(font): _check_text(font, "Tests/images/variation_adobe.png", 11) for name in ["Bold", b"Bold"]: font.set_variation_by_name(name) - _check_text(font, "Tests/images/variation_adobe_name.png", 11) + assert font.getname()[1] == "Bold" + _check_text(font, "Tests/images/variation_adobe_name.png", 16) font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) _check_text(font, "Tests/images/variation_tiny.png", 40) for name in ["200", b"200"]: font.set_variation_by_name(name) + assert font.getname()[1] == "200" _check_text(font, "Tests/images/variation_tiny_name.png", 40) @@ -1063,6 +1065,25 @@ def test_colr_mask(layout_engine): assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) +def test_woff2(layout_engine): + try: + font = ImageFont.truetype( + "Tests/fonts/OpenSans.woff2", + size=64, + layout_engine=layout_engine, + ) + except OSError as e: + assert str(e) in ("unimplemented feature", "unknown file format") + pytest.skip("FreeType compiled without brotli or WOFF2 support") + + im = Image.new("RGB", (350, 100), "white") + d = ImageDraw.Draw(im) + + d.text((15, 5), "OpenSans", "black", font=font) + + assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) + + def test_fill_deprecation(font): with pytest.warns(DeprecationWarning): font.getmask2("Hello world", fill=Image.core.fill) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa2291582..317db4c01 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess import sys @@ -33,7 +34,9 @@ class TestImageGrab: @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") def test_grab_no_xcb(self): - if sys.platform not in ("win32", "darwin"): + if sys.platform not in ("win32", "darwin") and not shutil.which( + "gnome-screenshot" + ): with pytest.raises(OSError) as e: ImageGrab.grab() assert str(e.value).startswith("Pillow was built without XCB support") @@ -61,9 +64,13 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ) p.communicate() else: - with pytest.raises(NotImplementedError) as e: - ImageGrab.grabclipboard() - assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" + if not shutil.which("wl-paste"): + with pytest.raises( + NotImplementedError, + match="wl-paste or xclip is required for" + r" ImageGrab.grabclipboard\(\) on Linux", + ): + ImageGrab.grabclipboard() return ImageGrab.grabclipboard() diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 185e477ec..3de7ec30f 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -34,7 +34,7 @@ def test_numpy_to_image(): # Check supported 1-bit integer formats assert_image(to_image(bool, 1, 1), "1", TEST_IMAGE_SIZE) - assert_image(to_image(numpy.bool8, 1, 1), "1", TEST_IMAGE_SIZE) + assert_image(to_image(numpy.bool_, 1, 1), "1", TEST_IMAGE_SIZE) # Check supported 8-bit integer formats assert_image(to_image(numpy.uint8), "L", TEST_IMAGE_SIZE) @@ -193,7 +193,7 @@ def test_putdata(): "dtype", ( bool, - numpy.bool8, + numpy.bool_, numpy.int8, numpy.int16, numpy.int32, diff --git a/docs/Makefile b/docs/Makefile index 458299aac..d32d25a3c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,11 +15,12 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others 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: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " serve to start a local server for viewing docs" + @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @@ -39,42 +40,49 @@ help: @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" +.PHONY: clean clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet sphinx sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph furo olefile + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph +.PHONY: html html: $(MAKE) install-sphinx $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." +.PHONY: dirhtml dirhtml: $(MAKE) install-sphinx $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." +.PHONY: singlehtml singlehtml: $(MAKE) install-sphinx $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." +.PHONY: pickle pickle: $(MAKE) install-sphinx $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." +.PHONY: json json: $(MAKE) install-sphinx $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." +.PHONY: htmlhelp htmlhelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @@ -82,6 +90,7 @@ htmlhelp: @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." +.PHONY: qthelp qthelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @@ -92,6 +101,7 @@ qthelp: @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" +.PHONY: devhelp devhelp: $(MAKE) install-sphinx $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @@ -102,12 +112,14 @@ devhelp: @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PillowPILfork" @echo "# devhelp" +.PHONY: epub epub: $(MAKE) install-sphinx $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." +.PHONY: latex latex: $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @@ -116,6 +128,7 @@ latex: @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." +.PHONY: latexpdf latexpdf: $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @@ -123,18 +136,21 @@ latexpdf: $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." +.PHONY: text text: $(MAKE) install-sphinx $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." +.PHONY: man man: $(MAKE) install-sphinx $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." +.PHONY: texinfo texinfo: $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @@ -143,6 +159,7 @@ texinfo: @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." +.PHONY: info info: $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @@ -150,18 +167,21 @@ info: make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." +.PHONY: gettext gettext: $(MAKE) install-sphinx $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." +.PHONY: changes changes: $(MAKE) install-sphinx $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." +.PHONY: linkcheck linkcheck: $(MAKE) install-sphinx $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @@ -169,14 +189,17 @@ linkcheck: @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." +.PHONY: doctest doctest: $(MAKE) install-sphinx $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +.PHONY: livehtml livehtml: html livereload $(BUILDDIR)/html -p 33233 +.PHONY: serve serve: cd $(BUILDDIR)/html; $(PYTHON) -m http.server diff --git a/docs/conf.py b/docs/conf.py index bc67d9368..04823e2d7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,12 +27,13 @@ needs_sphinx = "2.4" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "sphinx_copybutton", - "sphinx_issues", - "sphinx_removed_in", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinx_issues", + "sphinx_removed_in", "sphinxext.opengraph", ] diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index ec3938b36..26451533e 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -211,13 +211,16 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not a DDS file") + msg = "not a DDS file" + raise SyntaxError(msg) (header_size,) = struct.unpack("`_ and packages + are tested by the ports team with all supported FreeBSD versions. -macOS Installation -^^^^^^^^^^^^^^^^^^ - -We provide binaries for macOS for each of the supported Python -versions in the wheel format. These include support for all optional -libraries except libimagequant. Raqm support requires -FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - -Linux Installation -^^^^^^^^^^^^^^^^^^ - -We provide binaries for Linux for each of the supported Python -versions in the manylinux wheel format. These include support for all -optional libraries except libimagequant. Raqm support requires -FriBiDi to be installed separately:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow - -Most major Linux distributions, including Fedora, Ubuntu and ArchLinux -also include Pillow in packages that previously contained PIL e.g. -``python-imaging``. Debian splits it into two packages, ``python3-pil`` -and ``python3-pil.imagetk``. - -FreeBSD Installation -^^^^^^^^^^^^^^^^^^^^ - -Pillow can be installed on FreeBSD via the official Ports or Packages systems: - -**Ports**:: - - cd /usr/ports/graphics/py-pillow && make install clean - -**Packages**:: - - pkg install py38-pillow - -.. note:: - - The `Pillow FreeBSD port - `_ and packages - are tested by the ports team with all supported FreeBSD versions. - +.. _Building on Linux: +.. _Building on macOS: +.. _Building on Windows: +.. _Building on Windows using MSYS2/MinGW: +.. _Building on FreeBSD: +.. _Building on Android: Building From Source -------------------- -Download and extract the `compressed archive from PyPI`_. - -.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ - .. _external-libraries: External Libraries @@ -140,14 +143,14 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.4** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5** * **libfreetype** provides type related services * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**. + above uses liblcms2. Tested with **1.19** and **2.7-2.14**. * **libwebp** provides the WebP format. @@ -191,7 +194,141 @@ Many of Pillow's features require external libraries: * **libxcb** provides X11 screengrab support. -Once you have installed the prerequisites, run:: +.. tab:: Linux + + If you didn't build Python from source, make sure you have Python's + development libraries installed. + + In Debian or Ubuntu:: + + sudo apt-get install python3-dev python3-setuptools + + In Fedora, the command is:: + + sudo dnf install python3-devel redhat-rpm-config + + In Alpine, the command is:: + + sudo apk add python3-dev py3-setuptools + + .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. + + Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: + + sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ + libharfbuzz-dev libfribidi-dev libxcb1-dev + + To install libraqm, ``sudo apt-get install meson`` and then see + ``depends/install_raqm.sh``. + + Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: + + sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ + freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ + harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel + + Note that the package manager may be yum or DNF, depending on the + exact distribution. + + Prerequisites are installed for **Alpine** with:: + + sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ + libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ + libxcb-dev libpng-dev + + See also the ``Dockerfile``\s in the Test Infrastructure repo + (https://github.com/python-pillow/docker-images) for a known working + install process for other tested distros. + +.. tab:: macOS + + The Xcode command line tools are required to compile portions of + Pillow. The tools are installed by running ``xcode-select --install`` + from the command line. The command line tools are required even if you + have the full Xcode package installed. It may be necessary to run + ``sudo xcodebuild -license`` to accept the license prior to using the + tools. + + The easiest way to install external libraries is via `Homebrew + `_. After you install Homebrew, run:: + + brew install libjpeg libtiff little-cms2 openjpeg webp + + To install libraqm on macOS use Homebrew to install its dependencies:: + + brew install freetype harfbuzz fribidi + + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + +.. tab:: Windows + + We recommend you use prebuilt wheels from PyPI. + If you wish to compile Pillow manually, you can use the build scripts + in the ``winbuild`` directory used for CI testing and development. + These scripts require Visual Studio 2017 or newer and NASM. + + The scripts also install Pillow from the local copy of the source code, so the + `Installing`_ instructions will not be necessary afterwards. + +.. tab:: Windows using MSYS2/MinGW + + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + + Make sure you have Python and GCC installed:: + + pacman -S \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-python3 \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools + + Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: + + pacman -S \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-zlib \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libraqm + +.. tab:: FreeBSD + + .. Note:: Only FreeBSD 10 and 11 tested + + Make sure you have Python's development libraries installed:: + + sudo pkg install python3 + + Prerequisites are installed on **FreeBSD 10 or 11** with:: + + sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb + + Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + +.. tab:: Android + + Basic Android support has been added for compilation within the Termux + environment. The dependencies can be installed by:: + + pkg install -y python ndk-sysroot clang make \ + libjpeg-turbo + + This has been tested within the Termux app on ChromeOS, on x86. + +Installing +^^^^^^^^^^ + +Once you have installed the prerequisites, to install Pillow from the source +code on PyPI, run:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow --no-binary :all: @@ -211,9 +348,19 @@ prerequisites, it may be necessary to manually clear the pip cache or build without cache using the ``--no-cache-dir`` option to force a build with newly installed external libraries. +If you would like to install from a local copy of the source code instead, you +can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` +or download and extract the `compressed archive from PyPI`_. + +After navigating to the Pillow directory, run:: + + python3 -m pip install --upgrade pip + python3 -m pip install . + +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/#files Build Options -^^^^^^^^^^^^^ +""""""""""""" * Environment variable: ``MAX_CONCURRENCY=n``. Pillow can use multiprocessing to build the extension. Setting ``MAX_CONCURRENCY`` @@ -256,157 +403,6 @@ Sample usage:: python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" - -Building on macOS -^^^^^^^^^^^^^^^^^ - -The Xcode command line tools are required to compile portions of -Pillow. The tools are installed by running ``xcode-select --install`` -from the command line. The command line tools are required even if you -have the full Xcode package installed. It may be necessary to run -``sudo xcodebuild -license`` to accept the license prior to using the -tools. - -The easiest way to install external libraries is via `Homebrew -`_. After you install Homebrew, run:: - - brew install libjpeg libtiff little-cms2 openjpeg webp - -To install libraqm on macOS use Homebrew to install its dependencies:: - - brew install freetype harfbuzz fribidi - -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - -Now install Pillow with:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - -or from within the uncompressed source directory:: - - python3 -m pip install . - -Building on Windows -^^^^^^^^^^^^^^^^^^^ - -We recommend you use prebuilt wheels from PyPI. -If you wish to compile Pillow manually, you can use the build scripts -in the ``winbuild`` directory used for CI testing and development. -These scripts require Visual Studio 2017 or newer and NASM. - -Building on Windows using MSYS2/MinGW -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or -**MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. - -The following instructions target the 64-bit build, for 32-bit -replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. - -Make sure you have Python and GCC installed:: - - pacman -S \ - mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-python3 \ - mingw-w64-x86_64-python3-pip \ - mingw-w64-x86_64-python3-setuptools - -Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: - - pacman -S \ - mingw-w64-x86_64-libjpeg-turbo \ - mingw-w64-x86_64-zlib \ - mingw-w64-x86_64-libtiff \ - mingw-w64-x86_64-freetype \ - mingw-w64-x86_64-lcms2 \ - mingw-w64-x86_64-libwebp \ - mingw-w64-x86_64-openjpeg2 \ - mingw-w64-x86_64-libimagequant \ - mingw-w64-x86_64-libraqm - -Now install Pillow with:: - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow --no-binary :all: - - -Building on FreeBSD -^^^^^^^^^^^^^^^^^^^ - -.. Note:: Only FreeBSD 10 and 11 tested - -Make sure you have Python's development libraries installed:: - - sudo pkg install python3 - -Prerequisites are installed on **FreeBSD 10 or 11** with:: - - sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb - -Then see ``depends/install_raqm_cmake.sh`` to install libraqm. - - -Building on Linux -^^^^^^^^^^^^^^^^^ - -If you didn't build Python from source, make sure you have Python's -development libraries installed. - -In Debian or Ubuntu:: - - sudo apt-get install python3-dev python3-setuptools - -In Fedora, the command is:: - - sudo dnf install python3-devel redhat-rpm-config - -In Alpine, the command is:: - - sudo apk add python3-dev py3-setuptools - -.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. - -Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: - - sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ - libharfbuzz-dev libfribidi-dev libxcb1-dev - -To install libraqm, ``sudo apt-get install meson`` and then see -``depends/install_raqm.sh``. - -Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: - - sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ - freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ - harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel - -Note that the package manager may be yum or DNF, depending on the -exact distribution. - -Prerequisites are installed for **Alpine** with:: - - sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ - libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ - libxcb-dev libpng-dev - -See also the ``Dockerfile``\s in the Test Infrastructure repo -(https://github.com/python-pillow/docker-images) for a known working -install process for other tested distros. - -Building on Android -^^^^^^^^^^^^^^^^^^^ - -Basic Android support has been added for compilation within the Termux -environment. The dependencies can be installed by:: - - pkg install -y python ndk-sysroot clang make \ - libjpeg-turbo - -This has been tested within the Termux app on ChromeOS, on x86. - - Platform Support ---------------- @@ -440,10 +436,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ -| Fedora 35 | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 36 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 37 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | @@ -464,7 +460,7 @@ These platforms are built and tested for every change. | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ -| | 3.7, 3.8, 3.9 (Cygwin) | x86-64 | +| | 3.8, 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -482,11 +478,13 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | | +---------------------------+------------------+--------------+ -| | 3.7, 3.8, 3.9, 3.10 | 9.2.0 |x86-64 | +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |x86-64 | | +---------------------------+------------------+ | | | 3.6 | 8.4.0 | | +----------------------------------+---------------------------+------------------+--------------+ diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 794fa238f..464ab77ea 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,14 +4,56 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes two dictionaries which -provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes +which provide constants and clear-text names for various well-known EXIF tags. + +.. py:data:: Base + + >>> from PIL.ExifTags import Base + >>> Base.ImageDescription.value + 270 + >>> Base(270).name + 'ImageDescription' + +.. py:data:: GPS + + >>> from PIL.ExifTags import GPS + >>> GPS.GPSDestLatitude.value + 20 + >>> GPS(20).name + 'GPSDestLatitude' + +.. py:data:: Interop + + >>> from PIL.ExifTags import Interop + >>> Interop.RelatedImageFileFormat.value + 4096 + >>> Interop(4096).name + 'RelatedImageFileFormat' + +.. py:data:: IFD + + >>> from PIL.ExifTags import IFD + >>> IFD.Exif.value + 34665 + >>> IFD(34665).name + 'Exif + +.. py:data:: LightSource + + >>> from PIL.ExifTags import LightSource + >>> LightSource.Unknown.value + 0 + >>> LightSource(0).name + 'Unknown' + +Two of these values are also exposed as dictionaries. .. py:data:: TAGS :type: dict The TAGS dictionary maps 16-bit integer EXIF tag enumerations to - descriptive string names. For instance: + descriptive string names. For instance: >>> from PIL.ExifTags import TAGS >>> TAGS[0x010e] @@ -20,8 +62,8 @@ provide constants and clear-text names for various well-known EXIF tags. .. py:data:: GPSTAGS :type: dict - The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to - descriptive string names. For instance: + The GPSTAGS dictionary maps 8-bit integer EXIF GPS enumerations to + descriptive string names. For instance: >>> from PIL.ExifTags import GPSTAGS >>> GPSTAGS[20] diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 25f98b767..9aa26916a 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -139,17 +139,50 @@ Functions must be the same as the image mode. If omitted, the mode defaults to the mode of the image. +Attributes +---------- + +.. py:attribute:: ImageDraw.fill + :type: bool + :value: False + + Selects whether :py:attr:`ImageDraw.ink` should be used as a fill or outline color. + +.. py:attribute:: ImageDraw.font + + The current default font. + + Can be set per instance:: + + from PIL import ImageDraw, ImageFont + draw = ImageDraw.Draw(image) + draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + + Or globally for all future ImageDraw instances:: + + from PIL import ImageDraw, ImageFont + ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + +.. py:attribute:: ImageDraw.fontmode + + The current font drawing mode. + + Set to ``"1"`` to disable antialiasing or ``"L"`` to enable it. + +.. py:attribute:: ImageDraw.ink + :type: int + + The internal representation of the current default color. + Methods ------- .. py:method:: ImageDraw.getfont() - Get the current default font. + Get the current default font, :py:attr:`ImageDraw.font`. - To set the default font for all future ImageDraw instances:: - - from PIL import ImageDraw, ImageFont - ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + If the current default font is ``None``, + it is initialized with :py:func:`.ImageFont.load_default`. :returns: An image font. diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index 5a4086748..fde2faae3 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -1,9 +1,6 @@ 9.3.0 ----- -Backwards Incompatible Changes -============================== - 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, ...]) +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 ======== +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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -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 decode the data in its natural CMYK mode, then convert it to RGB and rearrange the channels afterwards. Trying to load the data in an incorrect mode could -result in a segmentation fault. +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 ============= +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 `_. + +A huge thanks to Christoph for building Windows binaries for us for around a decade, +plus testing, and fixing over a hundred bug fixes along the way, in addition to building +and hosting unofficial Windows binaries for hundreds of Python projects! + Added DDS ATI1, ATI2 and BC6H reading ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images. +Release GIL when converting images using matrix operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python's Global Interpreter Lock is now released when converting images using matrix +operations. + Show all frames with ImageShow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, all frames will now be shown. + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst new file mode 100644 index 000000000..e4e1e40fe --- /dev/null +++ b/docs/releasenotes/9.4.0.rst @@ -0,0 +1,111 @@ +9.4.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Added start position for getmask and getmask2 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Text may render differently when starting at fractional coordinates, so +:py:meth:`.FreeTypeFont.getmask` and :py:meth:`.FreeTypeFont.getmask2` now +support a ``start`` argument. This tuple of horizontal and vertical offset +will be used internally by :py:meth:`.ImageDraw.text` to more accurately place +text at the ``xy`` coordinates. + +Added the ``exact`` encoding option for WebP +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``exact`` encoding option for WebP is now supported. The WebP encoder +removes the hidden RGB values for better compression by default in libwebp 0.5 +or later. By setting this option to ``True``, the encoder will keep the hidden +RGB values. + +Added ``signed`` option when saving JPEG2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the ``signed`` keyword argument is present and true when saving JPEG2000 +images, then tell the encoder to save the image as signed. + +Added IFD, Interop and LightSource ExifTags enums +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:data:`~PIL.ExifTags.IFD` has been added, allowing enums to be used with +:py:meth:`~PIL.Image.Exif.get_ifd`:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + print(im.getexif().get_ifd(ExifTags.IFD.Exif)) + +``IFD1`` can also be used with :py:meth:`~PIL.Image.Exif.get_ifd`, but it should +not be used in other contexts, as the enum value is only internally meaningful. + +:py:data:`~PIL.ExifTags.Interop` has been added for tags within the Interop IFD:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/flower.jpg") + interop_ifd = im.getexif().get_ifd(ExifTags.IFD.Interop) + print(interop_ifd.get(ExifTags.Interop.InteropIndex)) # R98 + +:py:data:`~PIL.ExifTags.LightSource` has been added for values within the LightSource +tag:: + + from PIL import Image, ExifTags + im = Image.open("Tests/images/iptc.jpg") + exif_ifd = im.getexif().get_ifd(ExifTags.IFD.Exif) + print(ExifTags.LightSource(exif_ifd[0x9208])) # LightSource.Unknown + +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +decoded for WEBP images through ``getxmp()``. + +Writing JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When saving a JPEG image, a comment can now be written from +:py:attr:`~PIL.Image.Image.info`, or by using an argument when saving:: + + im.save(out, comment="Test comment") + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Added support for DDS L and LA images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read and write L and LA DDS images in the uncompressed +format, known as "luminance" textures. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 8c436be3b..a2b588696 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.4.0 9.3.0 9.2.0 9.1.1 diff --git a/setup.cfg b/setup.cfg index 44feb25ff..b562e2934 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ docs = olefile sphinx>=2.4 sphinx-copybutton + sphinx-inline-tabs sphinx-issues>=3.0.1 sphinx-removed-in sphinxext-opengraph diff --git a/setup.py b/setup.py index 14c404752..243365681 100755 --- a/setup.py +++ b/setup.py @@ -15,9 +15,7 @@ import subprocess import sys import warnings -from setuptools import Extension -from setuptools import __version__ as setuptools_version -from setuptools import setup +from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -364,15 +362,15 @@ class pil_build_ext(build_ext): self.feature.required.discard(x) _dbg("Disabling %s", x) if getattr(self, f"enable_{x}"): - raise ValueError( - f"Conflicting options: --enable-{x} and --disable-{x}" - ) + msg = f"Conflicting options: --enable-{x} and --disable-{x}" + raise ValueError(msg) if x == "freetype": _dbg("--disable-freetype implies --disable-raqm") if getattr(self, "enable_raqm"): - raise ValueError( + msg = ( "Conflicting options: --enable-raqm and --disable-freetype" ) + raise ValueError(msg) setattr(self, "disable_raqm", True) if getattr(self, f"enable_{x}"): _dbg("Requiring %s", x) @@ -383,13 +381,11 @@ class pil_build_ext(build_ext): for x in ("raqm", "fribidi"): if getattr(self, f"vendor_{x}"): if getattr(self, "disable_raqm"): - raise ValueError( - f"Conflicting options: --vendor-{x} and --disable-raqm" - ) + msg = f"Conflicting options: --vendor-{x} and --disable-raqm" + raise ValueError(msg) if x == "fribidi" and not getattr(self, "vendor_raqm"): - raise ValueError( - f"Conflicting options: --vendor-{x} and not --vendor-raqm" - ) + msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" + raise ValueError(msg) _dbg("Using vendored version of %s", x) self.feature.vendor.add(x) @@ -852,7 +848,6 @@ class pil_build_ext(build_ext): sys.platform == "win32" and sys.version_info < (3, 9) and not (PLATFORM_PYPY or PLATFORM_MINGW) - and int(setuptools_version.split(".")[0]) < 60 ): defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) else: diff --git a/src/PIL/AniImagePlugin.py b/src/PIL/AniImagePlugin.py index 2a2bdad6a..c407824e3 100644 --- a/src/PIL/AniImagePlugin.py +++ b/src/PIL/AniImagePlugin.py @@ -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))]) 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 sizes, hotspots = [], [] @@ -154,7 +155,8 @@ def _write_multiple_frames(im: Image.Image, fp: BytesIO, filename: str): seq_offset = fp.tell() for i in seq: 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)) @@ -172,10 +174,12 @@ def _write_multiple_frames(im: Image.Image, fp: BytesIO, filename: str): if 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: 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: fp.write(o32(r)) @@ -220,12 +224,14 @@ def _write_info(im: Image.Image, fp: BytesIO, filename: str): if isinstance(inam, str): inam = inam.encode() 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): iart = iart.encode() 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"INAM" + o32(0)) @@ -335,7 +341,8 @@ class AniFile: listOffset = listOffset + lSize + 8 if self.anih is None: - raise SyntaxError("not an ANI file") + msg = "not an ANI file" + raise SyntaxError(msg) if self.seq is None: self.seq = [i for i in range(self.anih["nFrames"])] @@ -345,7 +352,8 @@ class AniFile: def frame(self, frame): 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() self.buf.seek(offset) @@ -406,7 +414,8 @@ class AniImageFile(ImageFile.ImageFile): @size.setter def size(self, value): if value not in self.info["sizes"]: - raise ValueError("This is not one of the allowed sizes of this image") + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) self._size = value def load(self): @@ -419,7 +428,8 @@ class AniImageFile(ImageFile.ImageFile): def seek(self, frame): 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.load() diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 102b72e1d..e0dd4dede 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -86,7 +86,8 @@ class BdfFontFile(FontFile.FontFile): s = fp.readline() if s[:13] != b"STARTFONT 2.1": - raise SyntaxError("not a valid BDF file") + msg = "not a valid BDF file" + raise SyntaxError(msg) props = {} comments = [] diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 533997737..1cc0d4b3c 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -65,7 +65,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) def unpack_565(i): @@ -278,7 +279,8 @@ class BlpImageFile(ImageFile.ImageFile): if self.magic in (b"BLP1", b"BLP2"): decoder = self.magic.decode() else: - raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}") + msg = f"Bad BLP magic {repr(self.magic)}" + raise BLPFormatError(msg) self.mode = "RGBA" if self._blp_alpha_depth else "RGB" self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] @@ -292,7 +294,8 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): self._read_blp_header() self._load() except struct.error as e: - raise OSError("Truncated BLP file") from e + msg = "Truncated BLP file" + raise OSError(msg) from e return -1, 0 def _read_blp_header(self): @@ -354,13 +357,11 @@ class BLP1Decoder(_BLPBaseDecoder): data = self._read_bgra(palette) self.set_as_raw(bytes(data)) else: - raise BLPFormatError( - f"Unsupported BLP encoding {repr(self._blp_encoding)}" - ) + msg = f"Unsupported BLP encoding {repr(self._blp_encoding)}" + raise BLPFormatError(msg) else: - raise BLPFormatError( - f"Unsupported BLP compression {repr(self._blp_encoding)}" - ) + msg = f"Unsupported BLP compression {repr(self._blp_encoding)}" + raise BLPFormatError(msg) def _decode_jpeg_stream(self): from .JpegImagePlugin import JpegImageFile @@ -373,6 +374,9 @@ class BLP1Decoder(_BLPBaseDecoder): data = BytesIO(data) image = JpegImageFile(data) 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() image = Image.merge("RGB", (b, g, r)) self.set_as_raw(image.tobytes()) @@ -412,16 +416,15 @@ class BLP2Decoder(_BLPBaseDecoder): for d in decode_dxt5(self._safe_read(linesize)): data += d else: - raise BLPFormatError( - f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" - ) + msg = f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}" + raise BLPFormatError(msg) else: - raise BLPFormatError(f"Unknown BLP encoding {repr(self._blp_encoding)}") + msg = f"Unknown BLP encoding {repr(self._blp_encoding)}" + raise BLPFormatError(msg) else: - raise BLPFormatError( - f"Unknown BLP compression {repr(self._blp_compression)}" - ) + msg = f"Unknown BLP compression {repr(self._blp_compression)}" + raise BLPFormatError(msg) self.set_as_raw(bytes(data)) @@ -457,7 +460,8 @@ class BLPEncoder(ImageFile.PyEncoder): def _save(im, fp, filename, save_all=False): if im.mode != "P": - raise ValueError("Unsupported BLP image mode") + msg = "Unsupported BLP image mode" + raise ValueError(msg) magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" fp.write(magic) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index b8332062e..e0498d2a8 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -146,7 +146,8 @@ class BmpImageFile(ImageFile.ImageFile): file_info["a_mask"], ) else: - raise OSError(f"Unsupported BMP header type ({file_info['header_size']})") + msg = f"Unsupported BMP header type ({file_info['header_size']})" + raise OSError(msg) # ------------------ Special case : header is reported 40, which # ---------------------- is shorter than real size for bpp >= 16 @@ -164,7 +165,8 @@ class BmpImageFile(ImageFile.ImageFile): # ---------------------- Check bit depth for unusual unsupported values self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: - raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})") + msg = f"Unsupported BMP pixel depth ({file_info['bits']})" + raise OSError(msg) # ---------------- Process BMP with Bitfields compression (not palette) decoder_name = "raw" @@ -205,9 +207,11 @@ class BmpImageFile(ImageFile.ImageFile): ): raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] else: - raise OSError("Unsupported BMP bitfields layout") + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) else: - raise OSError("Unsupported BMP bitfields layout") + msg = "Unsupported BMP bitfields layout" + raise OSError(msg) elif file_info["compression"] == self.RAW: try: 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): decoder_name = "bmp_rle" else: - raise OSError(f"Unsupported BMP compression ({file_info['compression']})") + msg = f"Unsupported BMP compression ({file_info['compression']})" + raise OSError(msg) # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): - raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})") + msg = f"Unsupported BMP Palette size ({file_info['colors']})" + raise OSError(msg) else: padding = file_info["palette_padding"] palette = read(padding * file_info["colors"]) @@ -274,7 +280,8 @@ class BmpImageFile(ImageFile.ImageFile): head_data = self.fp.read(14) # choke if the file does not have the required magic bytes if not _accept(head_data): - raise SyntaxError("Not a BMP file") + msg = "Not a BMP file" + raise SyntaxError(msg) # read the start position of the BMP image data (u32) offset = i32(head_data, 10) # load bitmap information (offset=raster info) @@ -386,7 +393,8 @@ def _save(im, fp, filename, bitmap_header=True): try: rawmode, bits, colors = SAVE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as BMP") from e + msg = f"cannot write mode {im.mode} as BMP" + raise OSError(msg) from e info = im.encoderinfo @@ -414,7 +422,8 @@ def _save(im, fp, filename, bitmap_header=True): offset = 14 + header + colors * 4 file_size = offset + image if file_size > 2**32 - 1: - raise ValueError("File size is too large for the BMP format") + msg = "File size is too large for the BMP format" + raise ValueError(msg) fp.write( b"BM" # file type (magic) + o32(file_size) # file size diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 9510f733e..a0da1b786 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -42,7 +42,8 @@ class BufrStubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(4)): - raise SyntaxError("Not a BUFR file") + msg = "Not a BUFR file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class BufrStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("BUFR save handler not installed") + msg = "BUFR save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index add2cdc3a..e142d6dc7 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -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))]) 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 sizes, hotspots = [], [] @@ -117,7 +118,8 @@ class CurFile(IcoImagePlugin.IcoFile): # check if CUR s = buf.read(6) if not _accept(s): - raise SyntaxError("not a CUR file") + msg = "not a CUR file" + raise SyntaxError(msg) self.buf = buf self.entry = [] @@ -211,7 +213,8 @@ class CurImageFile(IcoImagePlugin.IcoImageFile): if len(self.ico.entry) > 0: self.size = self.ico.entry[0]["dim"] else: - raise TypeError("No cursors were found") + msg = "No cursors were found" + raise TypeError(msg) self.load() diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index aeed1e7c7..81c0314f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -47,7 +47,8 @@ class DcxImageFile(PcxImageFile): # Header s = self.fp.read(4) if not _accept(s): - raise SyntaxError("not a DCX file") + msg = "not a DCX file" + raise SyntaxError(msg) # Component directory self._offset = [] diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index eea6e3153..a946daeaa 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -114,13 +114,16 @@ class DdsImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not a DDS file") + msg = "not a DDS file" + raise SyntaxError(msg) (header_size,) = struct.unpack(" 255: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) try: m = split.match(s) except re.error as e: - raise SyntaxError("not an EPS file") from e + msg = "not an EPS file" + raise SyntaxError(msg) from e if m: k, v = m.group(1, 2) @@ -268,7 +271,8 @@ class EpsImageFile(ImageFile.ImageFile): # tools mistakenly put in the Comments section pass else: - raise OSError("bad EPS header") + msg = "bad EPS header" + raise OSError(msg) s_raw = fp.readline() s = s_raw.strip("\r\n") @@ -282,7 +286,8 @@ class EpsImageFile(ImageFile.ImageFile): while s[:1] == "%": if len(s) > 255: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) if s[:11] == "%ImageData:": # Encoded bitmapped image. @@ -306,7 +311,8 @@ class EpsImageFile(ImageFile.ImageFile): break if not box: - raise OSError("cannot determine EPS bounding box") + msg = "cannot determine EPS bounding box" + raise OSError(msg) def _find_offset(self, fp): @@ -326,7 +332,8 @@ class EpsImageFile(ImageFile.ImageFile): offset = i32(s, 4) length = i32(s, 8) else: - raise SyntaxError("not an EPS file") + msg = "not an EPS file" + raise SyntaxError(msg) return length, offset @@ -365,7 +372,8 @@ def _save(im, fp, filename, eps=1): elif im.mode == "CMYK": operator = (8, 4, b"false 4 colorimage") else: - raise ValueError("image mode is not supported") + msg = "image mode is not supported" + raise ValueError(msg) if eps: # diff --git a/src/PIL/ExifTags.py b/src/PIL/ExifTags.py index 7da2ddae5..2347c6d4c 100644 --- a/src/PIL/ExifTags.py +++ b/src/PIL/ExifTags.py @@ -14,318 +14,367 @@ This module provides constants and clear-text names for various well-known EXIF tags. """ +from enum import IntEnum -TAGS = { + +class Base(IntEnum): # possibly incomplete - 0x0001: "InteropIndex", - 0x000B: "ProcessingSoftware", - 0x00FE: "NewSubfileType", - 0x00FF: "SubfileType", - 0x0100: "ImageWidth", - 0x0101: "ImageLength", - 0x0102: "BitsPerSample", - 0x0103: "Compression", - 0x0106: "PhotometricInterpretation", - 0x0107: "Thresholding", - 0x0108: "CellWidth", - 0x0109: "CellLength", - 0x010A: "FillOrder", - 0x010D: "DocumentName", - 0x010E: "ImageDescription", - 0x010F: "Make", - 0x0110: "Model", - 0x0111: "StripOffsets", - 0x0112: "Orientation", - 0x0115: "SamplesPerPixel", - 0x0116: "RowsPerStrip", - 0x0117: "StripByteCounts", - 0x0118: "MinSampleValue", - 0x0119: "MaxSampleValue", - 0x011A: "XResolution", - 0x011B: "YResolution", - 0x011C: "PlanarConfiguration", - 0x011D: "PageName", - 0x0120: "FreeOffsets", - 0x0121: "FreeByteCounts", - 0x0122: "GrayResponseUnit", - 0x0123: "GrayResponseCurve", - 0x0124: "T4Options", - 0x0125: "T6Options", - 0x0128: "ResolutionUnit", - 0x0129: "PageNumber", - 0x012D: "TransferFunction", - 0x0131: "Software", - 0x0132: "DateTime", - 0x013B: "Artist", - 0x013C: "HostComputer", - 0x013D: "Predictor", - 0x013E: "WhitePoint", - 0x013F: "PrimaryChromaticities", - 0x0140: "ColorMap", - 0x0141: "HalftoneHints", - 0x0142: "TileWidth", - 0x0143: "TileLength", - 0x0144: "TileOffsets", - 0x0145: "TileByteCounts", - 0x014A: "SubIFDs", - 0x014C: "InkSet", - 0x014D: "InkNames", - 0x014E: "NumberOfInks", - 0x0150: "DotRange", - 0x0151: "TargetPrinter", - 0x0152: "ExtraSamples", - 0x0153: "SampleFormat", - 0x0154: "SMinSampleValue", - 0x0155: "SMaxSampleValue", - 0x0156: "TransferRange", - 0x0157: "ClipPath", - 0x0158: "XClipPathUnits", - 0x0159: "YClipPathUnits", - 0x015A: "Indexed", - 0x015B: "JPEGTables", - 0x015F: "OPIProxy", - 0x0200: "JPEGProc", - 0x0201: "JpegIFOffset", - 0x0202: "JpegIFByteCount", - 0x0203: "JpegRestartInterval", - 0x0205: "JpegLosslessPredictors", - 0x0206: "JpegPointTransforms", - 0x0207: "JpegQTables", - 0x0208: "JpegDCTables", - 0x0209: "JpegACTables", - 0x0211: "YCbCrCoefficients", - 0x0212: "YCbCrSubSampling", - 0x0213: "YCbCrPositioning", - 0x0214: "ReferenceBlackWhite", - 0x02BC: "XMLPacket", - 0x1000: "RelatedImageFileFormat", - 0x1001: "RelatedImageWidth", - 0x1002: "RelatedImageLength", - 0x4746: "Rating", - 0x4749: "RatingPercent", - 0x800D: "ImageID", - 0x828D: "CFARepeatPatternDim", - 0x828E: "CFAPattern", - 0x828F: "BatteryLevel", - 0x8298: "Copyright", - 0x829A: "ExposureTime", - 0x829D: "FNumber", - 0x83BB: "IPTCNAA", - 0x8649: "ImageResources", - 0x8769: "ExifOffset", - 0x8773: "InterColorProfile", - 0x8822: "ExposureProgram", - 0x8824: "SpectralSensitivity", - 0x8825: "GPSInfo", - 0x8827: "ISOSpeedRatings", - 0x8828: "OECF", - 0x8829: "Interlace", - 0x882A: "TimeZoneOffset", - 0x882B: "SelfTimerMode", - 0x8830: "SensitivityType", - 0x8831: "StandardOutputSensitivity", - 0x8832: "RecommendedExposureIndex", - 0x8833: "ISOSpeed", - 0x8834: "ISOSpeedLatitudeyyy", - 0x8835: "ISOSpeedLatitudezzz", - 0x9000: "ExifVersion", - 0x9003: "DateTimeOriginal", - 0x9004: "DateTimeDigitized", - 0x9010: "OffsetTime", - 0x9011: "OffsetTimeOriginal", - 0x9012: "OffsetTimeDigitized", - 0x9101: "ComponentsConfiguration", - 0x9102: "CompressedBitsPerPixel", - 0x9201: "ShutterSpeedValue", - 0x9202: "ApertureValue", - 0x9203: "BrightnessValue", - 0x9204: "ExposureBiasValue", - 0x9205: "MaxApertureValue", - 0x9206: "SubjectDistance", - 0x9207: "MeteringMode", - 0x9208: "LightSource", - 0x9209: "Flash", - 0x920A: "FocalLength", - 0x920B: "FlashEnergy", + InteropIndex = 0x0001 + ProcessingSoftware = 0x000B + NewSubfileType = 0x00FE + SubfileType = 0x00FF + ImageWidth = 0x0100 + ImageLength = 0x0101 + BitsPerSample = 0x0102 + Compression = 0x0103 + PhotometricInterpretation = 0x0106 + Thresholding = 0x0107 + CellWidth = 0x0108 + CellLength = 0x0109 + FillOrder = 0x010A + DocumentName = 0x010D + ImageDescription = 0x010E + Make = 0x010F + Model = 0x0110 + StripOffsets = 0x0111 + Orientation = 0x0112 + SamplesPerPixel = 0x0115 + RowsPerStrip = 0x0116 + StripByteCounts = 0x0117 + MinSampleValue = 0x0118 + MaxSampleValue = 0x0119 + XResolution = 0x011A + YResolution = 0x011B + PlanarConfiguration = 0x011C + PageName = 0x011D + FreeOffsets = 0x0120 + FreeByteCounts = 0x0121 + GrayResponseUnit = 0x0122 + GrayResponseCurve = 0x0123 + T4Options = 0x0124 + T6Options = 0x0125 + ResolutionUnit = 0x0128 + PageNumber = 0x0129 + TransferFunction = 0x012D + Software = 0x0131 + DateTime = 0x0132 + Artist = 0x013B + HostComputer = 0x013C + Predictor = 0x013D + WhitePoint = 0x013E + PrimaryChromaticities = 0x013F + ColorMap = 0x0140 + HalftoneHints = 0x0141 + TileWidth = 0x0142 + TileLength = 0x0143 + TileOffsets = 0x0144 + TileByteCounts = 0x0145 + SubIFDs = 0x014A + InkSet = 0x014C + InkNames = 0x014D + NumberOfInks = 0x014E + DotRange = 0x0150 + TargetPrinter = 0x0151 + ExtraSamples = 0x0152 + SampleFormat = 0x0153 + SMinSampleValue = 0x0154 + SMaxSampleValue = 0x0155 + TransferRange = 0x0156 + ClipPath = 0x0157 + XClipPathUnits = 0x0158 + YClipPathUnits = 0x0159 + Indexed = 0x015A + JPEGTables = 0x015B + OPIProxy = 0x015F + JPEGProc = 0x0200 + JpegIFOffset = 0x0201 + JpegIFByteCount = 0x0202 + JpegRestartInterval = 0x0203 + JpegLosslessPredictors = 0x0205 + JpegPointTransforms = 0x0206 + JpegQTables = 0x0207 + JpegDCTables = 0x0208 + JpegACTables = 0x0209 + YCbCrCoefficients = 0x0211 + YCbCrSubSampling = 0x0212 + YCbCrPositioning = 0x0213 + ReferenceBlackWhite = 0x0214 + XMLPacket = 0x02BC + RelatedImageFileFormat = 0x1000 + RelatedImageWidth = 0x1001 + RelatedImageLength = 0x1002 + Rating = 0x4746 + RatingPercent = 0x4749 + ImageID = 0x800D + CFARepeatPatternDim = 0x828D + BatteryLevel = 0x828F + Copyright = 0x8298 + ExposureTime = 0x829A + FNumber = 0x829D + IPTCNAA = 0x83BB + ImageResources = 0x8649 + ExifOffset = 0x8769 + InterColorProfile = 0x8773 + ExposureProgram = 0x8822 + SpectralSensitivity = 0x8824 + GPSInfo = 0x8825 + ISOSpeedRatings = 0x8827 + OECF = 0x8828 + Interlace = 0x8829 + TimeZoneOffset = 0x882A + SelfTimerMode = 0x882B + SensitivityType = 0x8830 + StandardOutputSensitivity = 0x8831 + RecommendedExposureIndex = 0x8832 + ISOSpeed = 0x8833 + ISOSpeedLatitudeyyy = 0x8834 + ISOSpeedLatitudezzz = 0x8835 + ExifVersion = 0x9000 + DateTimeOriginal = 0x9003 + DateTimeDigitized = 0x9004 + OffsetTime = 0x9010 + OffsetTimeOriginal = 0x9011 + OffsetTimeDigitized = 0x9012 + ComponentsConfiguration = 0x9101 + CompressedBitsPerPixel = 0x9102 + ShutterSpeedValue = 0x9201 + ApertureValue = 0x9202 + BrightnessValue = 0x9203 + ExposureBiasValue = 0x9204 + MaxApertureValue = 0x9205 + SubjectDistance = 0x9206 + MeteringMode = 0x9207 + LightSource = 0x9208 + Flash = 0x9209 + FocalLength = 0x920A + Noise = 0x920D + ImageNumber = 0x9211 + SecurityClassification = 0x9212 + ImageHistory = 0x9213 + TIFFEPStandardID = 0x9216 + MakerNote = 0x927C + UserComment = 0x9286 + SubsecTime = 0x9290 + SubsecTimeOriginal = 0x9291 + SubsecTimeDigitized = 0x9292 + AmbientTemperature = 0x9400 + Humidity = 0x9401 + Pressure = 0x9402 + WaterDepth = 0x9403 + Acceleration = 0x9404 + CameraElevationAngle = 0x9405 + XPTitle = 0x9C9B + XPComment = 0x9C9C + XPAuthor = 0x9C9D + XPKeywords = 0x9C9E + XPSubject = 0x9C9F + FlashPixVersion = 0xA000 + ColorSpace = 0xA001 + ExifImageWidth = 0xA002 + ExifImageHeight = 0xA003 + RelatedSoundFile = 0xA004 + ExifInteroperabilityOffset = 0xA005 + FlashEnergy = 0xA20B + SpatialFrequencyResponse = 0xA20C + FocalPlaneXResolution = 0xA20E + FocalPlaneYResolution = 0xA20F + FocalPlaneResolutionUnit = 0xA210 + SubjectLocation = 0xA214 + ExposureIndex = 0xA215 + SensingMethod = 0xA217 + FileSource = 0xA300 + SceneType = 0xA301 + CFAPattern = 0xA302 + CustomRendered = 0xA401 + ExposureMode = 0xA402 + WhiteBalance = 0xA403 + DigitalZoomRatio = 0xA404 + FocalLengthIn35mmFilm = 0xA405 + SceneCaptureType = 0xA406 + GainControl = 0xA407 + Contrast = 0xA408 + Saturation = 0xA409 + Sharpness = 0xA40A + DeviceSettingDescription = 0xA40B + SubjectDistanceRange = 0xA40C + ImageUniqueID = 0xA420 + CameraOwnerName = 0xA430 + BodySerialNumber = 0xA431 + LensSpecification = 0xA432 + LensMake = 0xA433 + LensModel = 0xA434 + LensSerialNumber = 0xA435 + CompositeImage = 0xA460 + CompositeImageCount = 0xA461 + CompositeImageExposureTimes = 0xA462 + Gamma = 0xA500 + PrintImageMatching = 0xC4A5 + DNGVersion = 0xC612 + DNGBackwardVersion = 0xC613 + UniqueCameraModel = 0xC614 + LocalizedCameraModel = 0xC615 + CFAPlaneColor = 0xC616 + CFALayout = 0xC617 + LinearizationTable = 0xC618 + BlackLevelRepeatDim = 0xC619 + BlackLevel = 0xC61A + BlackLevelDeltaH = 0xC61B + BlackLevelDeltaV = 0xC61C + WhiteLevel = 0xC61D + DefaultScale = 0xC61E + DefaultCropOrigin = 0xC61F + DefaultCropSize = 0xC620 + ColorMatrix1 = 0xC621 + ColorMatrix2 = 0xC622 + CameraCalibration1 = 0xC623 + CameraCalibration2 = 0xC624 + ReductionMatrix1 = 0xC625 + ReductionMatrix2 = 0xC626 + AnalogBalance = 0xC627 + AsShotNeutral = 0xC628 + AsShotWhiteXY = 0xC629 + BaselineExposure = 0xC62A + BaselineNoise = 0xC62B + BaselineSharpness = 0xC62C + BayerGreenSplit = 0xC62D + LinearResponseLimit = 0xC62E + CameraSerialNumber = 0xC62F + LensInfo = 0xC630 + ChromaBlurRadius = 0xC631 + AntiAliasStrength = 0xC632 + ShadowScale = 0xC633 + DNGPrivateData = 0xC634 + MakerNoteSafety = 0xC635 + CalibrationIlluminant1 = 0xC65A + CalibrationIlluminant2 = 0xC65B + BestQualityScale = 0xC65C + RawDataUniqueID = 0xC65D + OriginalRawFileName = 0xC68B + OriginalRawFileData = 0xC68C + ActiveArea = 0xC68D + MaskedAreas = 0xC68E + AsShotICCProfile = 0xC68F + AsShotPreProfileMatrix = 0xC690 + CurrentICCProfile = 0xC691 + CurrentPreProfileMatrix = 0xC692 + ColorimetricReference = 0xC6BF + CameraCalibrationSignature = 0xC6F3 + ProfileCalibrationSignature = 0xC6F4 + AsShotProfileName = 0xC6F6 + NoiseReductionApplied = 0xC6F7 + ProfileName = 0xC6F8 + ProfileHueSatMapDims = 0xC6F9 + ProfileHueSatMapData1 = 0xC6FA + ProfileHueSatMapData2 = 0xC6FB + ProfileToneCurve = 0xC6FC + ProfileEmbedPolicy = 0xC6FD + ProfileCopyright = 0xC6FE + ForwardMatrix1 = 0xC714 + ForwardMatrix2 = 0xC715 + PreviewApplicationName = 0xC716 + PreviewApplicationVersion = 0xC717 + PreviewSettingsName = 0xC718 + PreviewSettingsDigest = 0xC719 + PreviewColorSpace = 0xC71A + PreviewDateTime = 0xC71B + RawImageDigest = 0xC71C + OriginalRawFileDigest = 0xC71D + SubTileBlockSize = 0xC71E + RowInterleaveFactor = 0xC71F + ProfileLookTableDims = 0xC725 + ProfileLookTableData = 0xC726 + OpcodeList1 = 0xC740 + OpcodeList2 = 0xC741 + OpcodeList3 = 0xC74E + NoiseProfile = 0xC761 + + +"""Maps EXIF tags to tag names.""" +TAGS = { + **{i.value: i.name for i in Base}, 0x920C: "SpatialFrequencyResponse", - 0x920D: "Noise", - 0x9211: "ImageNumber", - 0x9212: "SecurityClassification", - 0x9213: "ImageHistory", 0x9214: "SubjectLocation", 0x9215: "ExposureIndex", + 0x828E: "CFAPattern", + 0x920B: "FlashEnergy", 0x9216: "TIFF/EPStandardID", - 0x927C: "MakerNote", - 0x9286: "UserComment", - 0x9290: "SubsecTime", - 0x9291: "SubsecTimeOriginal", - 0x9292: "SubsecTimeDigitized", - 0x9400: "AmbientTemperature", - 0x9401: "Humidity", - 0x9402: "Pressure", - 0x9403: "WaterDepth", - 0x9404: "Acceleration", - 0x9405: "CameraElevationAngle", - 0x9C9B: "XPTitle", - 0x9C9C: "XPComment", - 0x9C9D: "XPAuthor", - 0x9C9E: "XPKeywords", - 0x9C9F: "XPSubject", - 0xA000: "FlashPixVersion", - 0xA001: "ColorSpace", - 0xA002: "ExifImageWidth", - 0xA003: "ExifImageHeight", - 0xA004: "RelatedSoundFile", - 0xA005: "ExifInteroperabilityOffset", - 0xA20B: "FlashEnergy", - 0xA20C: "SpatialFrequencyResponse", - 0xA20E: "FocalPlaneXResolution", - 0xA20F: "FocalPlaneYResolution", - 0xA210: "FocalPlaneResolutionUnit", - 0xA214: "SubjectLocation", - 0xA215: "ExposureIndex", - 0xA217: "SensingMethod", - 0xA300: "FileSource", - 0xA301: "SceneType", - 0xA302: "CFAPattern", - 0xA401: "CustomRendered", - 0xA402: "ExposureMode", - 0xA403: "WhiteBalance", - 0xA404: "DigitalZoomRatio", - 0xA405: "FocalLengthIn35mmFilm", - 0xA406: "SceneCaptureType", - 0xA407: "GainControl", - 0xA408: "Contrast", - 0xA409: "Saturation", - 0xA40A: "Sharpness", - 0xA40B: "DeviceSettingDescription", - 0xA40C: "SubjectDistanceRange", - 0xA420: "ImageUniqueID", - 0xA430: "CameraOwnerName", - 0xA431: "BodySerialNumber", - 0xA432: "LensSpecification", - 0xA433: "LensMake", - 0xA434: "LensModel", - 0xA435: "LensSerialNumber", - 0xA460: "CompositeImage", - 0xA461: "CompositeImageCount", - 0xA462: "CompositeImageExposureTimes", - 0xA500: "Gamma", - 0xC4A5: "PrintImageMatching", - 0xC612: "DNGVersion", - 0xC613: "DNGBackwardVersion", - 0xC614: "UniqueCameraModel", - 0xC615: "LocalizedCameraModel", - 0xC616: "CFAPlaneColor", - 0xC617: "CFALayout", - 0xC618: "LinearizationTable", - 0xC619: "BlackLevelRepeatDim", - 0xC61A: "BlackLevel", - 0xC61B: "BlackLevelDeltaH", - 0xC61C: "BlackLevelDeltaV", - 0xC61D: "WhiteLevel", - 0xC61E: "DefaultScale", - 0xC61F: "DefaultCropOrigin", - 0xC620: "DefaultCropSize", - 0xC621: "ColorMatrix1", - 0xC622: "ColorMatrix2", - 0xC623: "CameraCalibration1", - 0xC624: "CameraCalibration2", - 0xC625: "ReductionMatrix1", - 0xC626: "ReductionMatrix2", - 0xC627: "AnalogBalance", - 0xC628: "AsShotNeutral", - 0xC629: "AsShotWhiteXY", - 0xC62A: "BaselineExposure", - 0xC62B: "BaselineNoise", - 0xC62C: "BaselineSharpness", - 0xC62D: "BayerGreenSplit", - 0xC62E: "LinearResponseLimit", - 0xC62F: "CameraSerialNumber", - 0xC630: "LensInfo", - 0xC631: "ChromaBlurRadius", - 0xC632: "AntiAliasStrength", - 0xC633: "ShadowScale", - 0xC634: "DNGPrivateData", - 0xC635: "MakerNoteSafety", - 0xC65A: "CalibrationIlluminant1", - 0xC65B: "CalibrationIlluminant2", - 0xC65C: "BestQualityScale", - 0xC65D: "RawDataUniqueID", - 0xC68B: "OriginalRawFileName", - 0xC68C: "OriginalRawFileData", - 0xC68D: "ActiveArea", - 0xC68E: "MaskedAreas", - 0xC68F: "AsShotICCProfile", - 0xC690: "AsShotPreProfileMatrix", - 0xC691: "CurrentICCProfile", - 0xC692: "CurrentPreProfileMatrix", - 0xC6BF: "ColorimetricReference", - 0xC6F3: "CameraCalibrationSignature", - 0xC6F4: "ProfileCalibrationSignature", - 0xC6F6: "AsShotProfileName", - 0xC6F7: "NoiseReductionApplied", - 0xC6F8: "ProfileName", - 0xC6F9: "ProfileHueSatMapDims", - 0xC6FA: "ProfileHueSatMapData1", - 0xC6FB: "ProfileHueSatMapData2", - 0xC6FC: "ProfileToneCurve", - 0xC6FD: "ProfileEmbedPolicy", - 0xC6FE: "ProfileCopyright", - 0xC714: "ForwardMatrix1", - 0xC715: "ForwardMatrix2", - 0xC716: "PreviewApplicationName", - 0xC717: "PreviewApplicationVersion", - 0xC718: "PreviewSettingsName", - 0xC719: "PreviewSettingsDigest", - 0xC71A: "PreviewColorSpace", - 0xC71B: "PreviewDateTime", - 0xC71C: "RawImageDigest", - 0xC71D: "OriginalRawFileDigest", - 0xC71E: "SubTileBlockSize", - 0xC71F: "RowInterleaveFactor", - 0xC725: "ProfileLookTableDims", - 0xC726: "ProfileLookTableData", - 0xC740: "OpcodeList1", - 0xC741: "OpcodeList2", - 0xC74E: "OpcodeList3", - 0xC761: "NoiseProfile", } -"""Maps EXIF tags to tag names.""" -GPSTAGS = { - 0: "GPSVersionID", - 1: "GPSLatitudeRef", - 2: "GPSLatitude", - 3: "GPSLongitudeRef", - 4: "GPSLongitude", - 5: "GPSAltitudeRef", - 6: "GPSAltitude", - 7: "GPSTimeStamp", - 8: "GPSSatellites", - 9: "GPSStatus", - 10: "GPSMeasureMode", - 11: "GPSDOP", - 12: "GPSSpeedRef", - 13: "GPSSpeed", - 14: "GPSTrackRef", - 15: "GPSTrack", - 16: "GPSImgDirectionRef", - 17: "GPSImgDirection", - 18: "GPSMapDatum", - 19: "GPSDestLatitudeRef", - 20: "GPSDestLatitude", - 21: "GPSDestLongitudeRef", - 22: "GPSDestLongitude", - 23: "GPSDestBearingRef", - 24: "GPSDestBearing", - 25: "GPSDestDistanceRef", - 26: "GPSDestDistance", - 27: "GPSProcessingMethod", - 28: "GPSAreaInformation", - 29: "GPSDateStamp", - 30: "GPSDifferential", - 31: "GPSHPositioningError", -} +class GPS(IntEnum): + GPSVersionID = 0 + GPSLatitudeRef = 1 + GPSLatitude = 2 + GPSLongitudeRef = 3 + GPSLongitude = 4 + GPSAltitudeRef = 5 + GPSAltitude = 6 + GPSTimeStamp = 7 + GPSSatellites = 8 + GPSStatus = 9 + GPSMeasureMode = 10 + GPSDOP = 11 + GPSSpeedRef = 12 + GPSSpeed = 13 + GPSTrackRef = 14 + GPSTrack = 15 + GPSImgDirectionRef = 16 + GPSImgDirection = 17 + GPSMapDatum = 18 + GPSDestLatitudeRef = 19 + GPSDestLatitude = 20 + GPSDestLongitudeRef = 21 + GPSDestLongitude = 22 + GPSDestBearingRef = 23 + GPSDestBearing = 24 + GPSDestDistanceRef = 25 + GPSDestDistance = 26 + GPSProcessingMethod = 27 + GPSAreaInformation = 28 + GPSDateStamp = 29 + GPSDifferential = 30 + GPSHPositioningError = 31 + + """Maps EXIF GPS tags to tag names.""" +GPSTAGS = {i.value: i.name for i in GPS} + + +class Interop(IntEnum): + InteropIndex = 1 + InteropVersion = 2 + RelatedImageFileFormat = 4096 + RelatedImageWidth = 4097 + RleatedImageHeight = 4098 + + +class IFD(IntEnum): + Exif = 34665 + GPSInfo = 34853 + Makernote = 37500 + Interop = 40965 + IFD1 = -1 + + +class LightSource(IntEnum): + Unknown = 0 + Daylight = 1 + Fluorescent = 2 + Tungsten = 3 + Flash = 4 + Fine = 9 + Cloudy = 10 + Shade = 11 + DaylightFluorescent = 12 + DayWhiteFluorescent = 13 + CoolWhiteFluorescent = 14 + WhiteFluorescent = 15 + StandardLightA = 17 + StandardLightB = 18 + StandardLightC = 19 + D55 = 20 + D65 = 21 + D75 = 22 + D50 = 23 + ISO = 24 + Other = 255 diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index c16300efa..536bc1fe6 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -28,7 +28,8 @@ class FitsImageFile(ImageFile.ImageFile): while True: header = self.fp.read(80) if not header: - raise OSError("Truncated FITS file") + msg = "Truncated FITS file" + raise OSError(msg) keyword = header[:8].strip() if keyword == b"END": break @@ -36,12 +37,14 @@ class FitsImageFile(ImageFile.ImageFile): if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): - raise SyntaxError("Not a FITS file") + msg = "Not a FITS file" + raise SyntaxError(msg) headers[keyword] = value naxis = int(headers[b"NAXIS"]) if naxis == 0: - raise ValueError("No image data") + msg = "No image data" + raise ValueError(msg) elif naxis == 1: self._size = 1, int(headers[b"NAXIS1"]) else: diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 440240a99..86eb2d5a2 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -67,7 +67,8 @@ class FITSStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): - raise OSError("FITS save handler not installed") + msg = "FITS save handler not installed" + raise OSError(msg) # -------------------------------------------------------------------- diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 908bed9f4..66681939d 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -50,7 +50,8 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): - raise SyntaxError("not an FLI/FLC file") + msg = "not an FLI/FLC file" + raise SyntaxError(msg) # frames self.n_frames = i16(s, 6) @@ -141,7 +142,8 @@ class FliImageFile(ImageFile.ImageFile): self.load() if frame != self.__frame + 1: - raise ValueError(f"cannot seek to frame {frame}") + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) self.__frame = frame # move to next frame diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index a55376d0e..8ddc6b40b 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -60,10 +60,12 @@ class FpxImageFile(ImageFile.ImageFile): try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: - raise SyntaxError("not an FPX file; invalid OLE file") from e + msg = "not an FPX file; invalid OLE file" + raise SyntaxError(msg) from e if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": - raise SyntaxError("not an FPX file; bad root CLSID") + msg = "not an FPX file; bad root CLSID" + raise SyntaxError(msg) self._open_index(1) @@ -99,7 +101,8 @@ class FpxImageFile(ImageFile.ImageFile): colors = [] bands = i32(s, 4) if bands > 4: - raise OSError("Invalid number of bands") + msg = "Invalid number of bands" + raise OSError(msg) for i in range(bands): # note: for now, we ignore the "uncalibrated" flag colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) @@ -141,7 +144,8 @@ class FpxImageFile(ImageFile.ImageFile): length = i32(s, 32) if size != self.size: - raise OSError("subimage mismatch") + msg = "subimage mismatch" + raise OSError(msg) # get tile descriptors fp.seek(28 + offset) @@ -217,7 +221,8 @@ class FpxImageFile(ImageFile.ImageFile): self.tile_prefix = self.jpeg[jpeg_tables] else: - raise OSError("unknown/invalid compression") + msg = "unknown/invalid compression" + raise OSError(msg) x = x + xtile if x >= xsize: diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 1b714eb4f..c7c32252b 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -73,7 +73,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) class FtexImageFile(ImageFile.ImageFile): @@ -82,7 +83,8 @@ class FtexImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not an FTEX file") + msg = "not an FTEX file" + raise SyntaxError(msg) struct.unpack(" 100: - raise SyntaxError("bad palette file") + msg = "bad palette file" + raise SyntaxError(msg) v = tuple(map(int, s.split()[:3])) if len(v) != 3: - raise ValueError("bad palette entry") + msg = "bad palette entry" + raise ValueError(msg) self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 4575f8237..2088eb7b0 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -42,7 +42,8 @@ class GribStubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(8)): - raise SyntaxError("Not a GRIB file") + msg = "Not a GRIB file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class GribStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("GRIB save handler not installed") + msg = "GRIB save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index df11cf2a6..d6f283739 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -42,7 +42,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() if not _accept(self.fp.read(8)): - raise SyntaxError("Not an HDF file") + msg = "Not an HDF file" + raise SyntaxError(msg) self.fp.seek(offset) @@ -60,7 +61,8 @@ class HDF5StubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("HDF5 save handler not installed") + msg = "HDF5 save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index fa192f053..e76d0c35a 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -42,7 +42,8 @@ def read_32t(fobj, start_length, size): fobj.seek(start) sig = fobj.read(4) if sig != b"\x00\x00\x00\x00": - raise SyntaxError("Unknown signature, expecting 0x00000000") + msg = "Unknown signature, expecting 0x00000000" + raise SyntaxError(msg) return read_32(fobj, (start + 4, length - 4), size) @@ -82,7 +83,8 @@ def read_32(fobj, start_length, size): if bytesleft <= 0: break if bytesleft != 0: - raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]") + msg = f"Error reading channel [{repr(bytesleft)} left]" + raise SyntaxError(msg) band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) im.im.putband(band.im, band_ix) return {"RGB": im} @@ -113,10 +115,11 @@ def read_png_or_jpeg2000(fobj, start_length, size): or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: - raise ValueError( + msg = ( "Unsupported icon subimage format (rebuild PIL " "with JPEG 2000 support to fix this)" ) + raise ValueError(msg) # j2k, jpc or j2c fobj.seek(start) jp2kstream = fobj.read(length) @@ -127,7 +130,8 @@ def read_png_or_jpeg2000(fobj, start_length, size): im = im.convert("RGBA") return {"RGBA": im} else: - raise ValueError("Unsupported icon subimage format") + msg = "Unsupported icon subimage format" + raise ValueError(msg) class IcnsFile: @@ -168,12 +172,14 @@ class IcnsFile: self.fobj = fobj sig, filesize = nextheader(fobj) if not _accept(sig): - raise SyntaxError("not an icns file") + msg = "not an icns file" + raise SyntaxError(msg) i = HEADERSIZE while i < filesize: sig, blocksize = nextheader(fobj) if blocksize <= 0: - raise SyntaxError("invalid block header") + msg = "invalid block header" + raise SyntaxError(msg) i += HEADERSIZE blocksize -= HEADERSIZE dct[sig] = (i, blocksize) @@ -192,7 +198,8 @@ class IcnsFile: def bestsize(self): sizes = self.itersizes() if not sizes: - raise SyntaxError("No 32bit icon resources found") + msg = "No 32bit icon resources found" + raise SyntaxError(msg) return max(sizes) def dataforsize(self, size): @@ -275,7 +282,8 @@ class IcnsImageFile(ImageFile.ImageFile): if value in simple_sizes: info_size = self.info["sizes"][simple_sizes.index(value)] if info_size not in self.info["sizes"]: - raise ValueError("This is not one of the allowed sizes of this image") + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) self._size = value def load(self): diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 93b9dfdea..568e6d38d 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -127,7 +127,8 @@ class IcoFile: # check magic s = buf.read(6) if not _accept(s): - raise SyntaxError("not an ICO file") + msg = "not an ICO file" + raise SyntaxError(msg) self.buf = buf self.entry = [] @@ -316,7 +317,8 @@ class IcoImageFile(ImageFile.ImageFile): @size.setter def size(self, value): if value not in self.info["sizes"]: - raise ValueError("This is not one of the allowed sizes of this image") + msg = "This is not one of the allowed sizes of this image" + raise ValueError(msg) self._size = value def load(self): diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 31b0ff469..875a20326 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -126,7 +126,8 @@ class ImImageFile(ImageFile.ImageFile): # 100 bytes, this is (probably) not a text header. if b"\n" not in self.fp.read(100): - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) self.fp.seek(0) n = 0 @@ -153,7 +154,8 @@ class ImImageFile(ImageFile.ImageFile): s = s + self.fp.readline() if len(s) > 100: - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) if s[-2:] == b"\r\n": s = s[:-2] @@ -163,7 +165,8 @@ class ImImageFile(ImageFile.ImageFile): try: m = split.match(s) except re.error as e: - raise SyntaxError("not an IM file") from e + msg = "not an IM file" + raise SyntaxError(msg) from e if m: @@ -198,12 +201,12 @@ class ImImageFile(ImageFile.ImageFile): else: - raise SyntaxError( - "Syntax error in IM header: " + s.decode("ascii", "replace") - ) + msg = "Syntax error in IM header: " + s.decode("ascii", "replace") + raise SyntaxError(msg) if not n: - raise SyntaxError("Not an IM file") + msg = "Not an IM file" + raise SyntaxError(msg) # Basic attributes self._size = self.info[SIZE] @@ -213,7 +216,8 @@ class ImImageFile(ImageFile.ImageFile): while s and s[:1] != b"\x1A": s = self.fp.read(1) if not s: - raise SyntaxError("File truncated") + msg = "File truncated" + raise SyntaxError(msg) if LUT in self.info: # convert lookup table to palette or lut attribute @@ -332,7 +336,8 @@ def _save(im, fp, filename): try: image_type, rawmode = SAVE[im.mode] except KeyError as e: - raise ValueError(f"Cannot save {im.mode} images as IM") from e + msg = f"Cannot save {im.mode} images as IM" + raise ValueError(msg) from e frames = im.encoderinfo.get("frames", 1) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7faf0c248..b22060965 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -47,7 +47,14 @@ except ImportError: # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. # Use __version__ instead. -from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins +from . import ( + ExifTags, + ImageMode, + TiffTags, + UnidentifiedImageError, + __version__, + _plugins, +) from ._binary import i32le, o32be, o32le from ._deprecate import deprecate from ._util import DeferredError, is_path @@ -73,7 +80,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(name, 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) logger = logging.getLogger(__name__) @@ -100,11 +108,12 @@ try: from . import _imaging as core if __version__ != getattr(core, "PILLOW_VERSION", None): - raise ImportError( + msg = ( "The _imaging extension was built for another version of Pillow or PIL:\n" f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" f"Pillow version: {__version__}" ) + raise ImportError(msg) except ImportError as v: core = DeferredError(ImportError("The _imaging C module is not installed.")) @@ -399,7 +408,8 @@ def _getdecoder(mode, decoder_name, args, extra=()): # get decoder decoder = getattr(core, decoder_name + "_decoder") except AttributeError as e: - raise OSError(f"decoder {decoder_name} not available") from e + msg = f"decoder {decoder_name} not available" + raise OSError(msg) from e return decoder(mode, *args + extra) @@ -422,7 +432,8 @@ def _getencoder(mode, encoder_name, args, extra=()): # get encoder encoder = getattr(core, encoder_name + "_encoder") except AttributeError as e: - raise OSError(f"encoder {encoder_name} not available") from e + msg = f"encoder {encoder_name} not available" + raise OSError(msg) from e return encoder(mode, *args + extra) @@ -668,7 +679,8 @@ class Image: try: self.save(b, "PNG") except Exception as e: - raise ValueError("Could not save to PNG for display") from e + msg = "Could not save to PNG for display" + raise ValueError(msg) from e return b.getvalue() @property @@ -704,7 +716,6 @@ class Image: def __setstate__(self, state): Image.__init__(self) - self.tile = [] info, mode, size, palette, data = state self.info = info self.mode = mode @@ -761,7 +772,8 @@ class Image: if s: break if s < 0: - raise RuntimeError(f"encoder error {s} in tobytes") + msg = f"encoder error {s} in tobytes" + raise RuntimeError(msg) return b"".join(data) @@ -778,7 +790,8 @@ class Image: self.load() if self.mode != "1": - raise ValueError("not a bitmap") + msg = "not a bitmap" + raise ValueError(msg) data = self.tobytes("xbm") return b"".join( [ @@ -812,9 +825,11 @@ class Image: s = d.decode(data) if s[0] >= 0: - raise ValueError("not enough image data") + msg = "not enough image data" + raise ValueError(msg) if s[1] != 0: - raise ValueError("cannot decode image data") + msg = "cannot decode image data" + raise ValueError(msg) def load(self): """ @@ -935,7 +950,8 @@ class Image: if matrix: # matrix conversion if mode not in ("L", "RGB"): - raise ValueError("illegal conversion") + msg = "illegal conversion" + raise ValueError(msg) im = self.im.convert_matrix(mode, matrix) new = self._new(im) if has_transparency and self.im.bands == 3: @@ -1020,7 +1036,8 @@ class Image: elif isinstance(t, int): self.im.putpalettealpha(t, 0) else: - raise ValueError("Transparency for P mode should be bytes or int") + msg = "Transparency for P mode should be bytes or int" + raise ValueError(msg) if mode == "P" and palette == Palette.ADAPTIVE: im = self.im.quantize(colors) @@ -1070,7 +1087,8 @@ class Image: im = self.im.convert(modebase) im = im.convert(mode, dither) except KeyError as e: - raise ValueError("illegal conversion") from e + msg = "illegal conversion" + raise ValueError(msg) from e new_im = self._new(im) if mode == "P" and palette != Palette.ADAPTIVE: @@ -1145,20 +1163,21 @@ class Image: Quantize.LIBIMAGEQUANT, ): # Caller specified an invalid mode. - raise ValueError( + msg = ( "Fast Octree (method == 2) and libimagequant (method == 3) " "are the only valid methods for quantizing RGBA images" ) + raise ValueError(msg) if palette: # use palette from reference image palette.load() if palette.mode != "P": - raise ValueError("bad mode for palette image") + msg = "bad mode for palette image" + raise ValueError(msg) if self.mode != "RGB" and self.mode != "L": - raise ValueError( - "only RGB or L mode images can be quantized to a palette" - ) + msg = "only RGB or L mode images can be quantized to a palette" + raise ValueError(msg) im = self.im.convert("P", dither, palette.im) new_im = self._new(im) new_im.palette = palette.palette.copy() @@ -1204,9 +1223,11 @@ class Image: return self.copy() if box[2] < box[0]: - raise ValueError("Coordinate 'right' is less than 'left'") + msg = "Coordinate 'right' is less than 'left'" + raise ValueError(msg) elif box[3] < box[1]: - raise ValueError("Coordinate 'lower' is less than 'upper'") + msg = "Coordinate 'lower' is less than 'upper'" + raise ValueError(msg) self.load() return self._new(self._crop(self.im, box)) @@ -1274,9 +1295,8 @@ class Image: if isinstance(filter, Callable): filter = filter() if not hasattr(filter, "filter"): - raise TypeError( - "filter argument should be ImageFilter.Filter instance or class" - ) + msg = "filter argument should be ImageFilter.Filter instance or class" + raise TypeError(msg) multiband = isinstance(filter, ImageFilter.MultibandFilter) if self.im.bands == 1 or multiband: @@ -1447,6 +1467,49 @@ class Image: self._exif._loaded = False self.getexif() + def get_child_images(self): + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(513): + ifds.append((ifd1, exif._info.next)) + + offset = None + for ifd, ifd_offset in ifds: + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: + try: + thumbnail_offset += self._exif_offset + except AttributeError: + pass + self.fp.seek(thumbnail_offset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) + + with open(fp) as im: + if thumbnail_offset is None: + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self.fp.seek(offset) + return child_images + def getim(self): """ Returns a capsule that points to the internal image memory. @@ -1482,7 +1545,8 @@ class Image: def apply_transparency(self): """ If a P mode image has a "transparency" key in the info dictionary, - remove the key and apply the transparency to the palette instead. + remove the key and instead apply the transparency to the palette. + Otherwise, the image is unchanged. """ if self.mode != "P" or "transparency" not in self.info: return @@ -1641,7 +1705,8 @@ class Image: size = mask.size else: # FIXME: use self.size here? - raise ValueError("cannot determine region size; use 4-item box") + msg = "cannot determine region size; use 4-item box" + raise ValueError(msg) box += (box[0] + size[0], box[1] + size[1]) if isinstance(im, str): @@ -1680,15 +1745,20 @@ class Image: """ if not isinstance(source, (list, tuple)): - raise ValueError("Source must be a tuple") + msg = "Source must be a tuple" + raise ValueError(msg) if not isinstance(dest, (list, tuple)): - raise ValueError("Destination must be a tuple") + msg = "Destination must be a tuple" + raise ValueError(msg) if not len(source) in (2, 4): - raise ValueError("Source must be a 2 or 4-tuple") + msg = "Source must be a 2 or 4-tuple" + raise ValueError(msg) if not len(dest) == 2: - raise ValueError("Destination must be a 2-tuple") + msg = "Destination must be a 2-tuple" + raise ValueError(msg) if min(source) < 0: - raise ValueError("Source must be non-negative") + msg = "Source must be non-negative" + raise ValueError(msg) if len(source) == 2: source = source + im.size @@ -1753,7 +1823,8 @@ class Image: if self.mode == "F": # FIXME: _imaging returns a confusing error message for this case - raise ValueError("point operation not supported for this mode") + msg = "point operation not supported for this mode" + raise ValueError(msg) if mode != "F": lut = [round(i) for i in lut] @@ -1787,7 +1858,8 @@ class Image: self.pyaccess = None self.mode = self.im.mode except KeyError as e: - raise ValueError("illegal image mode") from e + msg = "illegal image mode" + raise ValueError(msg) from e if self.mode in ("LA", "PA"): band = 1 @@ -1797,7 +1869,8 @@ class Image: if isImageType(alpha): # alpha layer if alpha.mode not in ("1", "L"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) alpha.load() if alpha.mode == "1": alpha = alpha.convert("L") @@ -1853,7 +1926,8 @@ class Image: from . import ImagePalette if self.mode not in ("L", "LA", "P", "PA"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) if isinstance(data, ImagePalette.ImagePalette): palette = ImagePalette.raw(data.rawmode, data.palette) else: @@ -1922,7 +1996,8 @@ class Image: from . import ImagePalette if self.mode not in ("L", "P"): - raise ValueError("illegal image mode") + msg = "illegal image mode" + raise ValueError(msg) bands = 3 palette_mode = "RGB" @@ -2054,7 +2129,7 @@ class Image: Resampling.BOX, Resampling.HAMMING, ): - message = f"Unknown resampling filter ({resample})." + msg = f"Unknown resampling filter ({resample})." filters = [ f"{filter[1]} ({filter[0]})" @@ -2067,12 +2142,12 @@ class Image: (Resampling.HAMMING, "Image.Resampling.HAMMING"), ) ] - raise ValueError( - message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] - ) + msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + raise ValueError(msg) if reducing_gap is not None and reducing_gap < 1.0: - raise ValueError("reducing_gap must be 1.0 or greater") + msg = "reducing_gap must be 1.0 or greater" + raise ValueError(msg) size = tuple(size) @@ -2330,7 +2405,8 @@ class Image: try: format = EXTENSION[ext] except KeyError as e: - raise ValueError(f"unknown file extension: {ext}") from e + msg = f"unknown file extension: {ext}" + raise ValueError(msg) from e if format.upper() not in SAVE: init() @@ -2444,7 +2520,8 @@ class Image: try: channel = self.getbands().index(channel) except ValueError as e: - raise ValueError(f'The image has no channel "{channel}"') from e + msg = f'The image has no channel "{channel}"' + raise ValueError(msg) from e return self._new(self.im.getband(channel)) @@ -2615,7 +2692,8 @@ class Image: method, data = method.getdata() if data is None: - raise ValueError("missing method data") + msg = "missing method data" + raise ValueError(msg) im = new(self.mode, size, fillcolor) if self.mode == "P" and self.palette: @@ -2676,7 +2754,8 @@ class Image: ) else: - raise ValueError("unknown transformation method") + msg = "unknown transformation method" + raise ValueError(msg) if resample not in ( Resampling.NEAREST, @@ -2684,13 +2763,13 @@ class Image: Resampling.BICUBIC, ): if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): - message = { + msg = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.LANCZOS: "Image.Resampling.LANCZOS", }[resample] + f" ({resample}) cannot be used." else: - message = f"Unknown resampling filter ({resample})." + msg = f"Unknown resampling filter ({resample})." filters = [ f"{filter[1]} ({filter[0]})" @@ -2700,9 +2779,8 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), ) ] - raise ValueError( - message + " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] - ) + msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] + raise ValueError(msg) image.load() @@ -2741,7 +2819,8 @@ class Image: from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.toqimage(self) def toqpixmap(self): @@ -2749,7 +2828,8 @@ class Image: from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.toqpixmap(self) @@ -2797,11 +2877,14 @@ def _check_size(size): """ if not isinstance(size, (list, tuple)): - raise ValueError("Size must be a tuple") + msg = "Size must be a tuple" + raise ValueError(msg) if len(size) != 2: - raise ValueError("Size must be a tuple of length 2") + msg = "Size must be a tuple of length 2" + raise ValueError(msg) if size[0] < 0 or size[1] < 0: - raise ValueError("Width and height must be >= 0") + msg = "Width and height must be >= 0" + raise ValueError(msg) return True @@ -2987,11 +3070,13 @@ def fromarray(obj, mode=None): try: typekey = (1, 1) + shape[2:], arr["typestr"] except KeyError as e: - raise TypeError("Cannot handle this data type") from e + msg = "Cannot handle this data type" + raise TypeError(msg) from e try: mode, rawmode = _fromarray_typemap[typekey] except KeyError as e: - raise TypeError("Cannot handle this data type: %s, %s" % typekey) from e + msg = "Cannot handle this data type: %s, %s" % typekey + raise TypeError(msg) from e else: rawmode = mode if mode in ["1", "L", "I", "P", "F"]: @@ -3001,7 +3086,8 @@ def fromarray(obj, mode=None): else: ndmax = 4 if ndim > ndmax: - raise ValueError(f"Too many dimensions: {ndim} > {ndmax}.") + msg = f"Too many dimensions: {ndim} > {ndmax}." + raise ValueError(msg) size = 1 if ndim == 1 else shape[1], shape[0] if strides is not None: @@ -3018,7 +3104,8 @@ def fromqimage(im): from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.fromqimage(im) @@ -3027,7 +3114,8 @@ def fromqpixmap(im): from . import ImageQt if not ImageQt.qt_is_installed: - raise ImportError("Qt bindings are not installed") + msg = "Qt bindings are not installed" + raise ImportError(msg) return ImageQt.fromqpixmap(im) @@ -3065,10 +3153,11 @@ def _decompression_bomb_check(size): pixels = size[0] * size[1] if pixels > 2 * MAX_IMAGE_PIXELS: - raise DecompressionBombError( + msg = ( f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " "pixels, could be decompression bomb DOS attack." ) + raise DecompressionBombError(msg) if pixels > MAX_IMAGE_PIXELS: warnings.warn( @@ -3108,17 +3197,20 @@ def open(fp, mode="r", formats=None): """ if mode != "r": - raise ValueError(f"bad mode {repr(mode)}") + msg = f"bad mode {repr(mode)}" + raise ValueError(msg) elif isinstance(fp, io.StringIO): - raise ValueError( + msg = ( "StringIO cannot be used to open an image. " "Binary data must be used instead." ) + raise ValueError(msg) if formats is None: formats = ID elif not isinstance(formats, (list, tuple)): - raise TypeError("formats must be a list or tuple") + msg = "formats must be a list or tuple" + raise TypeError(msg) exclusive_fp = False filename = "" @@ -3183,9 +3275,8 @@ def open(fp, mode="r", formats=None): fp.close() for message in accept_warnings: warnings.warn(message) - raise UnidentifiedImageError( - "cannot identify image file %r" % (filename if filename else fp) - ) + msg = "cannot identify image file %r" % (filename if filename else fp) + raise UnidentifiedImageError(msg) # @@ -3276,12 +3367,15 @@ def merge(mode, bands): """ if getmodebands(mode) != len(bands) or "*" in mode: - raise ValueError("wrong number of bands") + msg = "wrong number of bands" + raise ValueError(msg) for band in bands[1:]: if band.mode != getmodetype(mode): - raise ValueError("mode mismatch") + msg = "mode mismatch" + raise ValueError(msg) if band.size != bands[0].size: - raise ValueError("size mismatch") + msg = "size mismatch" + raise ValueError(msg) for band in bands: band.load() return bands[0]._new(core.merge(mode, *[b.im for b in bands])) @@ -3368,8 +3462,7 @@ def registered_extensions(): Returns a dictionary containing all file extensions belonging to registered plugins """ - if not EXTENSION: - init() + init() return EXTENSION @@ -3503,6 +3596,7 @@ class Exif(MutableMapping): def __init__(self): self._data = {} + self._hidden_data = {} self._ifds = {} self._info = None self._loaded_exif = None @@ -3556,6 +3650,7 @@ class Exif(MutableMapping): return self._loaded_exif = data self._data.clear() + self._hidden_data.clear() self._ifds.clear() if data and data.startswith(b"Exif\x00\x00"): data = data[6:] @@ -3576,6 +3671,7 @@ class Exif(MutableMapping): def load_from_fp(self, fp, offset=None): self._loaded_exif = None self._data.clear() + self._hidden_data.clear() self._ifds.clear() # process dictionary @@ -3598,14 +3694,16 @@ class Exif(MutableMapping): merged_dict = dict(self) # get EXIF extension - if 0x8769 in self: - ifd = self._get_ifd_dict(self[0x8769]) + if ExifTags.IFD.Exif in self: + ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif]) if ifd: merged_dict.update(ifd) # GPS - if 0x8825 in self: - merged_dict[0x8825] = self._get_ifd_dict(self[0x8825]) + if ExifTags.IFD.GPSInfo in self: + merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( + self[ExifTags.IFD.GPSInfo] + ) return merged_dict @@ -3615,31 +3713,35 @@ class Exif(MutableMapping): head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): - if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict): + if tag in [ + ExifTags.IFD.Exif, + ExifTags.IFD.GPSInfo, + ] and not isinstance(value, dict): value = self.get_ifd(tag) if ( - tag == 0x8769 - and 0xA005 in value - and not isinstance(value[0xA005], dict) + tag == ExifTags.IFD.Exif + and ExifTags.IFD.Interop in value + and not isinstance(value[ExifTags.IFD.Interop], dict) ): value = value.copy() - value[0xA005] = self.get_ifd(0xA005) + value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) def get_ifd(self, tag): if tag not in self._ifds: - if tag in [0x8769, 0x8825]: - # exif, gpsinfo - if tag in self: - self._ifds[tag] = self._get_ifd_dict(self[tag]) - elif tag in [0xA005, 0x927C]: - # interop, makernote - if 0x8769 not in self._ifds: - self.get_ifd(0x8769) - tag_data = self._ifds[0x8769][tag] - if tag == 0x927C: - # makernote + if tag == ExifTags.IFD.IFD1: + if self._info is not None and self._info.next != 0: + self._ifds[tag] = self._get_ifd_dict(self._info.next) + elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: + offset = self._hidden_data.get(tag, self.get(tag)) + if offset is not None: + self._ifds[tag] = self._get_ifd_dict(offset) + elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: + if ExifTags.IFD.Exif not in self._ifds: + self.get_ifd(ExifTags.IFD.Exif) + tag_data = self._ifds[ExifTags.IFD.Exif][tag] + if tag == ExifTags.IFD.Makernote: from .TiffImagePlugin import ImageFileDirectory_v2 if tag_data[:8] == b"FUJIFILM": @@ -3715,9 +3817,22 @@ class Exif(MutableMapping): makernote = {0x1101: dict(self._fixup_dict(camerainfo))} self._ifds[tag] = makernote else: - # interop + # Interop self._ifds[tag] = self._get_ifd_dict(tag_data) - return self._ifds.get(tag, {}) + ifd = self._ifds.get(tag, {}) + if tag == ExifTags.IFD.Exif and self._hidden_data: + ifd = { + k: v + for (k, v) in ifd.items() + if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) + } + return ifd + + def hide_offsets(self): + for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): + if tag in self: + self._hidden_data[tag] = self[tag] + del self[tag] def __str__(self): if self._info is not None: diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 605252d5d..f87849680 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -124,7 +124,8 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) # @@ -191,7 +192,8 @@ class ImageCmsProfile: elif isinstance(profile, _imagingcms.CmsProfile): self._set(profile) else: - raise TypeError("Invalid type for Profile") + msg = "Invalid type for Profile" + raise TypeError(msg) def _set(self, profile, filename=None): self.profile = profile @@ -269,7 +271,8 @@ class ImageCmsTransform(Image.ImagePointHandler): def apply_in_place(self, im): im.load() if im.mode != self.output_mode: - raise ValueError("mode mismatch") # wrong output mode + msg = "mode mismatch" + raise ValueError(msg) # wrong output mode self.transform.apply(im.im.id, im.im.id) im.info["icc_profile"] = self.output_profile.tobytes() return im @@ -374,10 +377,12 @@ def profileToProfile( outputMode = im.mode if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError(f"flags must be an integer between 0 and {_MAX_FLAG}") + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -489,10 +494,12 @@ def buildTransform( """ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -591,10 +598,12 @@ def buildProofTransform( """ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): - raise PyCMSError("renderingIntent must be an integer between 0 and 3") + msg = "renderingIntent must be an integer between 0 and 3" + raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + raise PyCMSError(msg) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -705,17 +714,17 @@ def createProfile(colorSpace, colorTemp=-1): """ if colorSpace not in ["LAB", "XYZ", "sRGB"]: - raise PyCMSError( + msg = ( f"Color space not supported for on-the-fly profile creation ({colorSpace})" ) + raise PyCMSError(msg) if colorSpace == "LAB": try: colorTemp = float(colorTemp) except (TypeError, ValueError) as e: - raise PyCMSError( - f'Color temperature must be numeric, "{colorTemp}" not valid' - ) from e + msg = f'Color temperature must be numeric, "{colorTemp}" not valid' + raise PyCMSError(msg) from e try: return core.createProfile(colorSpace, colorTemp) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 9cbce4143..e184ed68d 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -33,7 +33,8 @@ def getrgb(color): :return: ``(red, green, blue[, alpha])`` """ if len(color) > 100: - raise ValueError("color specifier is too long") + msg = "color specifier is too long" + raise ValueError(msg) color = color.lower() rgb = colormap.get(color, None) @@ -115,7 +116,8 @@ def getrgb(color): m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) if m: return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4)) - raise ValueError(f"unknown color specifier: {repr(color)}") + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) def getcolor(color, mode): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 3a6063a6c..ce29a163b 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -69,7 +69,8 @@ class ImageDraw: if mode == "RGBA" and im.mode == "RGB": blend = 1 else: - raise ValueError("mode mismatch") + msg = "mode mismatch" + raise ValueError(msg) if mode == "P": self.palette = im.palette else: @@ -87,17 +88,25 @@ class ImageDraw: self.fontmode = "1" else: self.fontmode = "L" # aliasing is okay for other modes - self.fill = 0 + self.fill = False def getfont(self): """ Get the current default font. + To set the default font for this ImageDraw instance:: + + from PIL import ImageDraw, ImageFont + draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + To set the default font for all future ImageDraw instances:: from PIL import ImageDraw, ImageFont ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf") + If the current default font is ``None``, + it is initialized with ``ImageFont.load_default()``. + :returns: An image font.""" if not self.font: # FIXME: should add a font repository @@ -429,7 +438,8 @@ class ImageDraw: ) if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if font is None: font = self.getfont() @@ -444,7 +454,11 @@ class ImageDraw: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" - coord = xy + coord = [] + start = [] + for i in range(2): + coord.append(int(xy[i])) + start.append(math.modf(xy[i])[0]) try: mask, offset = font.getmask2( text, @@ -455,6 +469,7 @@ class ImageDraw: stroke_width=stroke_width, anchor=anchor, ink=ink, + start=start, *args, **kwargs, ) @@ -470,6 +485,7 @@ class ImageDraw: stroke_width, anchor, ink, + start=start, *args, **kwargs, ) @@ -482,7 +498,7 @@ class ImageDraw: # extract mask and set text alpha color, mask = mask, mask.getband(3) color.fillband(3, (ink >> 24) & 0xFF) - x, y = (int(c) for c in coord) + x, y = coord self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) else: self.draw.draw_bitmap(coord, mask, ink) @@ -520,14 +536,17 @@ class ImageDraw: embedded_color=False, ): if direction == "ttb": - raise ValueError("ttb direction is unsupported for multiline text") + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) if anchor is None: anchor = "la" elif len(anchor) != 2: - raise ValueError("anchor must be a 2 character string") + msg = "anchor must be a 2 character string" + raise ValueError(msg) elif anchor[1] in "tb": - raise ValueError("anchor not supported for multiline text") + msg = "anchor not supported for multiline text" + raise ValueError(msg) widths = [] max_width = 0 @@ -564,7 +583,8 @@ class ImageDraw: elif align == "right": left += width_difference else: - raise ValueError('align must be "left", "center" or "right"') + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) self.text( (left, top), @@ -658,9 +678,11 @@ class ImageDraw: ): """Get the length of a given string, in pixels with 1/64 precision.""" if self._multiline_check(text): - raise ValueError("can't measure length of multiline text") + msg = "can't measure length of multiline text" + raise ValueError(msg) if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if font is None: font = self.getfont() @@ -698,7 +720,8 @@ class ImageDraw: ): """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): - raise ValueError("Embedded color supported only in RGB and RGBA modes") + msg = "Embedded color supported only in RGB and RGBA modes" + raise ValueError(msg) if self._multiline_check(text): return self.multiline_textbbox( @@ -738,14 +761,17 @@ class ImageDraw: embedded_color=False, ): if direction == "ttb": - raise ValueError("ttb direction is unsupported for multiline text") + msg = "ttb direction is unsupported for multiline text" + raise ValueError(msg) if anchor is None: anchor = "la" elif len(anchor) != 2: - raise ValueError("anchor must be a 2 character string") + msg = "anchor must be a 2 character string" + raise ValueError(msg) elif anchor[1] in "tb": - raise ValueError("anchor not supported for multiline text") + msg = "anchor not supported for multiline text" + raise ValueError(msg) widths = [] max_width = 0 @@ -789,7 +815,8 @@ class ImageDraw: elif align == "right": left += width_difference else: - raise ValueError('align must be "left", "center" or "right"') + msg = 'align must be "left", "center" or "right"' + raise ValueError(msg) bbox_line = self.textbbox( (left, top), @@ -965,38 +992,44 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1. Error Handling # 1.1 Check `n_sides` has an appropriate value if not isinstance(n_sides, int): - raise TypeError("n_sides should be an int") + msg = "n_sides should be an int" + raise TypeError(msg) if n_sides < 3: - raise ValueError("n_sides should be an int > 2") + msg = "n_sides should be an int > 2" + raise ValueError(msg) # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - raise TypeError("bounding_circle should be a tuple") + msg = "bounding_circle should be a tuple" + raise TypeError(msg) if len(bounding_circle) == 3: *centroid, polygon_radius = bounding_circle elif len(bounding_circle) == 2: centroid, polygon_radius = bounding_circle else: - raise ValueError( + msg = ( "bounding_circle should contain 2D coordinates " "and a radius (e.g. (x, y, r) or ((x, y), r) )" ) + raise ValueError(msg) if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): - raise ValueError("bounding_circle should only contain numeric data") + msg = "bounding_circle should only contain numeric data" + raise ValueError(msg) if not len(centroid) == 2: - raise ValueError( - "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" - ) + msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" + raise ValueError(msg) if polygon_radius <= 0: - raise ValueError("bounding_circle radius should be > 0") + msg = "bounding_circle radius should be > 0" + raise ValueError(msg) # 1.3 Check `rotation` has an appropriate value if not isinstance(rotation, (int, float)): - raise ValueError("rotation should be an int or float") + msg = "rotation should be an int or float" + raise ValueError(msg) # 2. Define Helper Functions def _apply_rotation(point, degrees, centroid): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f281b9e14..12391955f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -63,12 +63,13 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`, def raise_oserror(error): try: - message = Image.core.getcodecstatus(error) + msg = Image.core.getcodecstatus(error) except AttributeError: - message = ERRORS.get(error) - if not message: - message = f"decoder error {error}" - raise OSError(message + " when reading image file") + msg = ERRORS.get(error) + if not msg: + msg = f"decoder error {error}" + msg += " when reading image file" + raise OSError(msg) def _tilesort(t): @@ -124,7 +125,8 @@ class ImageFile(Image.Image): raise SyntaxError(v) from v if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: - raise SyntaxError("not identified by this driver") + msg = "not identified by this driver" + raise SyntaxError(msg) except BaseException: # close the file only if we have opened it this constructor if self._exclusive_fp: @@ -137,6 +139,10 @@ class ImageFile(Image.Image): if self.format is not None: return Image.MIME.get(self.format.upper()) + def __setstate__(self, state): + self.tile = [] + super().__setstate__(state) + def verify(self): """Check file integrity""" @@ -150,7 +156,8 @@ class ImageFile(Image.Image): """Load image data based on tile list""" if self.tile is None: - raise OSError("cannot load this image") + msg = "cannot load this image" + raise OSError(msg) pixel = Image.Image.load(self) if not self.tile: @@ -245,16 +252,18 @@ class ImageFile(Image.Image): if LOAD_TRUNCATED_IMAGES: break else: - raise OSError("image file is truncated") from e + msg = "image file is truncated" + raise OSError(msg) from e if not s: # truncated jpeg if LOAD_TRUNCATED_IMAGES: break else: - raise OSError( + msg = ( "image file is truncated " f"({len(b)} bytes not processed)" ) + raise OSError(msg) b = b + s n, err_code = decoder.decode(b) @@ -310,7 +319,8 @@ class ImageFile(Image.Image): and frame >= self.n_frames + self._min_frame ) ): - raise EOFError("attempt to seek outside sequence") + msg = "attempt to seek outside sequence" + raise EOFError(msg) return self.tell() != frame @@ -324,12 +334,14 @@ class StubImageFile(ImageFile): """ def _open(self): - raise NotImplementedError("StubImageFile subclass must implement _open") + msg = "StubImageFile subclass must implement _open" + raise NotImplementedError(msg) def load(self): loader = self._load() if loader is None: - raise OSError(f"cannot find loader for this {self.format} file") + msg = f"cannot find loader for this {self.format} file" + raise OSError(msg) image = loader.load(self) assert image is not None # become the other object (!) @@ -339,7 +351,8 @@ class StubImageFile(ImageFile): def _load(self): """(Hook) Find actual image loader.""" - raise NotImplementedError("StubImageFile subclass must implement _load") + msg = "StubImageFile subclass must implement _load" + raise NotImplementedError(msg) class Parser: @@ -464,9 +477,11 @@ class Parser: self.feed(b"") self.data = self.decoder = None if not self.finished: - raise OSError("image was incomplete") + msg = "image was incomplete" + raise OSError(msg) if not self.image: - raise OSError("cannot parse this image") + msg = "cannot parse this image" + raise OSError(msg) if self.data: # incremental parsing not possible; reopen the file # not that we have all data @@ -531,7 +546,8 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): # slight speedup: compress to real file object s = encoder.encode_to_file(fh, bufsize) if s < 0: - raise OSError(f"encoder error {s} when writing image file") from exc + msg = f"encoder error {s} when writing image file" + raise OSError(msg) from exc finally: encoder.cleanup() @@ -554,7 +570,8 @@ def _safe_read(fp, size): if size <= SAFEBLOCK: data = fp.read(size) if len(data) < size: - raise OSError("Truncated File Read") + msg = "Truncated File Read" + raise OSError(msg) return data data = [] remaining_size = size @@ -565,7 +582,8 @@ def _safe_read(fp, size): data.append(block) remaining_size -= len(block) if sum(len(d) for d in data) < size: - raise OSError("Truncated File Read") + msg = "Truncated File Read" + raise OSError(msg) return b"".join(data) @@ -641,13 +659,15 @@ class PyCodec: self.state.ysize = y1 - y0 if self.state.xsize <= 0 or self.state.ysize <= 0: - raise ValueError("Size cannot be negative") + msg = "Size cannot be negative" + raise ValueError(msg) if ( self.state.xsize + self.state.xoff > self.im.size[0] or self.state.ysize + self.state.yoff > self.im.size[1] ): - raise ValueError("Tile cannot extend outside image") + msg = "Tile cannot extend outside image" + raise ValueError(msg) class PyDecoder(PyCodec): @@ -692,9 +712,11 @@ class PyDecoder(PyCodec): s = d.decode(data) if s[0] >= 0: - raise ValueError("not enough image data") + msg = "not enough image data" + raise ValueError(msg) if s[1] != 0: - raise ValueError("cannot decode image data") + msg = "cannot decode image data" + raise ValueError(msg) class PyEncoder(PyCodec): diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index e10c6fdf1..59e2c18b9 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -28,7 +28,8 @@ class MultibandFilter(Filter): class BuiltinFilter(MultibandFilter): def filter(self, image): if image.mode == "P": - raise ValueError("cannot filter palette images") + msg = "cannot filter palette images" + raise ValueError(msg) return image.filter(*self.filterargs) @@ -57,7 +58,8 @@ class Kernel(BuiltinFilter): # default scale is sum of kernel scale = functools.reduce(lambda a, b: a + b, kernel) if size[0] * size[1] != len(kernel): - raise ValueError("not enough coefficients in kernel") + msg = "not enough coefficients in kernel" + raise ValueError(msg) self.filterargs = size, scale, offset, kernel @@ -80,7 +82,8 @@ class RankFilter(Filter): def filter(self, image): if image.mode == "P": - raise ValueError("cannot filter palette images") + msg = "cannot filter palette images" + raise ValueError(msg) image = image.expand(self.size // 2, self.size // 2) return image.rankfilter(self.size, self.rank) @@ -355,7 +358,8 @@ class Color3DLUT(MultibandFilter): def __init__(self, size, table, channels=3, target_mode=None, **kwargs): if channels not in (3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) self.size = size = self._check_size(size) self.channels = channels self.mode = target_mode @@ -395,19 +399,21 @@ class Color3DLUT(MultibandFilter): table, raw_table = [], table for pixel in raw_table: if len(pixel) != channels: - raise ValueError( + msg = ( "The elements of the table should " - "have a length of {}.".format(channels) + f"have a length of {channels}." ) + raise ValueError(msg) table.extend(pixel) if wrong_size or len(table) != items * channels: - raise ValueError( + msg = ( "The table should have either channels * size**3 float items " "or size**3 items of channels-sized tuples with floats. " f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. " f"Actual length: {len(table)}" ) + raise ValueError(msg) self.table = table @staticmethod @@ -415,15 +421,15 @@ class Color3DLUT(MultibandFilter): try: _, _, _ = size except ValueError as e: - raise ValueError( - "Size should be either an integer or a tuple of three integers." - ) from e + msg = "Size should be either an integer or a tuple of three integers." + raise ValueError(msg) from e except TypeError: size = (size, size, size) size = [int(x) for x in size] for size_1d in size: if not 2 <= size_1d <= 65: - raise ValueError("Size should be in [2, 65] range.") + msg = "Size should be in [2, 65] range." + raise ValueError(msg) return size @classmethod @@ -441,7 +447,8 @@ class Color3DLUT(MultibandFilter): """ size_1d, size_2d, size_3d = cls._check_size(size) if channels not in (3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) table = [0] * (size_1d * size_2d * size_3d * channels) idx_out = 0 @@ -481,7 +488,8 @@ class Color3DLUT(MultibandFilter): lookup table. """ if channels not in (None, 3, 4): - raise ValueError("Only 3 or 4 output channels are supported") + msg = "Only 3 or 4 output channels are supported" + raise ValueError(msg) ch_in = self.channels ch_out = channels or ch_in size_1d, size_2d, size_3d = self.size diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b3a7cdb98..b144c3dd2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,6 +26,7 @@ # import base64 +import math import os import sys import warnings @@ -49,13 +50,15 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) class _ImagingFtNotInstalled: # module placeholder def __getattr__(self, id): - raise ImportError("The _imagingft C module is not installed") + msg = "The _imagingft C module is not installed" + raise ImportError(msg) try: @@ -104,7 +107,8 @@ class ImageFont: else: if image: image.close() - raise OSError("cannot find glyph data file") + msg = "cannot find glyph data file" + raise OSError(msg) self.file = fullname @@ -115,7 +119,8 @@ class ImageFont: # read PILfont header if file.readline() != b"PILfont\n": - raise SyntaxError("Not a PILfont file") + msg = "Not a PILfont file" + raise SyntaxError(msg) file.readline().split(b";") self.info = [] # FIXME: should be a dictionary while True: @@ -129,7 +134,8 @@ class ImageFont: # check image if image.mode not in ("1", "L"): - raise TypeError("invalid font image mode") + msg = "invalid font image mode" + raise TypeError(msg) image.load() @@ -588,6 +594,7 @@ class FreeTypeFont: stroke_width=0, anchor=None, ink=0, + start=None, ): """ Create a bitmap for the text. @@ -647,6 +654,11 @@ class FreeTypeFont: .. versionadded:: 8.0.0 + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ @@ -659,6 +671,7 @@ class FreeTypeFont: stroke_width=stroke_width, anchor=anchor, ink=ink, + start=start, )[0] def getmask2( @@ -672,6 +685,7 @@ class FreeTypeFont: stroke_width=0, anchor=None, ink=0, + start=None, *args, **kwargs, ): @@ -739,6 +753,11 @@ class FreeTypeFont: .. versionadded:: 8.0.0 + :param start: Tuple of horizontal and vertical offset, as text may render + differently when starting at fractional coordinates. + + .. versionadded:: 9.4.0 + :return: A tuple of an internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking @@ -750,12 +769,23 @@ class FreeTypeFont: size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) - size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 + if start is None: + start = (0, 0) + size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) im = fill("RGBA" if mode == "RGBA" else "L", size, 0) self.font.render( - text, im.id, mode, direction, features, language, stroke_width, ink + text, + im.id, + mode, + direction, + features, + language, + stroke_width, + ink, + start[0], + start[1], ) return im, offset @@ -792,7 +822,8 @@ class FreeTypeFont: try: names = self.font.getvarnames() except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e return [name.replace(b"\x00", b"") for name in names] def set_variation_by_name(self, name): @@ -803,7 +834,7 @@ class FreeTypeFont: names = self.get_variation_names() if not isinstance(name, bytes): name = name.encode() - index = names.index(name) + index = names.index(name) + 1 if index == getattr(self, "_last_variation_index", None): # When the same name is set twice in a row, @@ -822,7 +853,8 @@ class FreeTypeFont: try: axes = self.font.getvaraxes() except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e for axis in axes: axis["name"] = axis["name"].replace(b"\x00", b"") return axes @@ -835,7 +867,8 @@ class FreeTypeFont: try: self.font.setvaraxes(axes) except AttributeError as e: - raise NotImplementedError("FreeType 2.9.1 or greater is required") from e + msg = "FreeType 2.9.1 or greater is required" + raise NotImplementedError(msg) from e class TransposedFont: @@ -889,9 +922,8 @@ class TransposedFont: def getlength(self, text, *args, **kwargs): if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - raise ValueError( - "text length is undefined for text rotated by 90 or 270 degrees" - ) + msg = "text length is undefined for text rotated by 90 or 270 degrees" + raise ValueError(msg) return self.font.getlength(text, *args, **kwargs) @@ -1036,7 +1068,8 @@ def load_path(filename): return load(os.path.join(directory, filename)) except OSError: pass - raise OSError("cannot find font file") + msg = "cannot find font file" + raise OSError(msg) def load_default(): diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 38074cb1b..982f77f20 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -75,7 +75,8 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im # use xdisplay=None for default display on non-win32/macOS systems if not Image.core.HAVE_XCB: - raise OSError("Pillow was built without XCB support") + msg = "Pillow was built without XCB support" + raise OSError(msg) size, data = Image.core.grabscreen_x11(xdisplay) im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) if bbox: @@ -132,4 +133,17 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") + if shutil.which("wl-paste"): + args = ["wl-paste"] + elif shutil.which("xclip"): + args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] + else: + msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" + raise NotImplementedError(msg) + fh, filepath = tempfile.mkstemp() + subprocess.call(args, stdout=fh) + os.close(fh) + im = Image.open(filepath) + im.load() + os.unlink(filepath) + return im diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 09d9898d7..ac7d36b69 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -39,7 +39,8 @@ class _Operand: elif im1.im.mode in ("I", "F"): return im1.im else: - raise ValueError(f"unsupported mode: {im1.im.mode}") + msg = f"unsupported mode: {im1.im.mode}" + raise ValueError(msg) else: # argument was a constant if _isconstant(im1) and self.im.mode in ("1", "L", "I"): @@ -56,7 +57,8 @@ class _Operand: try: op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError as e: - raise TypeError(f"bad operand type for '{op}'") from e + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e _imagingmath.unop(op, out.im.id, im1.im.id) else: # binary operation @@ -80,7 +82,8 @@ class _Operand: try: op = getattr(_imagingmath, op + "_" + im1.mode) except AttributeError as e: - raise TypeError(f"bad operand type for '{op}'") from e + msg = f"bad operand type for '{op}'" + raise TypeError(msg) from e _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) return _Operand(out) @@ -249,7 +252,8 @@ def eval(expression, _dict={}, **kw): for name in code.co_names: if name not in args and name != "abs": - raise ValueError(f"'{name}' not allowed") + msg = f"'{name}' not allowed" + raise ValueError(msg) scan(compiled_code) out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 1e22c36a8..6fccc315b 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -81,7 +81,8 @@ class LutBuilder: ], } if op_name not in known_patterns: - raise Exception("Unknown pattern " + op_name + "!") + msg = "Unknown pattern " + op_name + "!" + raise Exception(msg) self.patterns = known_patterns[op_name] @@ -145,7 +146,8 @@ class LutBuilder: for p in self.patterns: m = re.search(r"(\w*):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", "")) if not m: - raise Exception('Syntax error in pattern "' + p + '"') + msg = 'Syntax error in pattern "' + p + '"' + raise Exception(msg) options = m.group(1) pattern = m.group(2) result = int(m.group(3)) @@ -193,10 +195,12 @@ class MorphOp: Returns a tuple of the number of changed pixels and the morphed image""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage @@ -208,10 +212,12 @@ class MorphOp: Returns a list of tuples of (x,y) coordinates of all matching pixels. See :ref:`coordinate-system`.""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): @@ -221,7 +227,8 @@ class MorphOp: of all matching pixels. See :ref:`coordinate-system`.""" if image.mode != "L": - raise ValueError("Image mode must be L") + msg = "Image mode must be L" + raise ValueError(msg) return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): @@ -231,12 +238,14 @@ class MorphOp: if len(self.lut) != LUT_SIZE: self.lut = None - raise Exception("Wrong size operator file!") + msg = "Wrong size operator file!" + raise Exception(msg) def save_lut(self, filename): """Save an operator to an mrl file""" if self.lut is None: - raise Exception("No operator loaded") + msg = "No operator loaded" + raise Exception(msg) with open(filename, "wb") as f: f.write(self.lut) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 443c540b6..e2168ce62 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -49,13 +49,15 @@ def _color(color, mode): def _lut(image, lut): if image.mode == "P": # FIXME: apply to lookup table, not image data - raise NotImplementedError("mode P support coming soon") + msg = "mode P support coming soon" + raise NotImplementedError(msg) elif image.mode in ("L", "RGB"): if image.mode == "RGB" and len(lut) == 256: lut = lut + lut + lut return image.point(lut) else: - raise OSError("not supported for this image mode") + msg = "not supported for this image mode" + raise OSError(msg) # @@ -332,7 +334,8 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): if factor == 1: return image.copy() elif factor <= 0: - raise ValueError("the factor must be greater than 0") + msg = "the factor must be greater than 0" + raise ValueError(msg) else: size = (round(factor * image.width), round(factor * image.height)) return image.resize(size, resample) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe76c86f4..fe0d32155 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -42,7 +42,8 @@ class ImagePalette: if size != 0: deprecate("The size parameter", 10, None) if size != len(self.palette): - raise ValueError("wrong palette size") + msg = "wrong palette size" + raise ValueError(msg) @property def palette(self): @@ -97,7 +98,8 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(self.palette, bytes): return self.palette arr = array.array("B", self.palette) @@ -112,14 +114,14 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(color, tuple): if self.mode == "RGB": if len(color) == 4: if color[3] != 255: - raise ValueError( - "cannot add non-opaque RGBA color to RGB palette" - ) + msg = "cannot add non-opaque RGBA color to RGB palette" + raise ValueError(msg) color = color[:3] elif self.mode == "RGBA": if len(color) == 3: @@ -147,7 +149,8 @@ class ImagePalette: index = i break if index >= 256: - raise ValueError("cannot allocate more than 256 colors") from e + msg = "cannot allocate more than 256 colors" + raise ValueError(msg) from e self.colors[color] = index if index * 3 < len(self.palette): self._palette = ( @@ -160,7 +163,8 @@ class ImagePalette: self.dirty = 1 return index else: - raise ValueError(f"unknown color specifier: {repr(color)}") + msg = f"unknown color specifier: {repr(color)}" + raise ValueError(msg) def save(self, fp): """Save palette to text file. @@ -168,7 +172,8 @@ class ImagePalette: .. warning:: This method is experimental. """ if self.rawmode: - raise ValueError("palette contains raw palette data") + msg = "palette contains raw palette data" + raise ValueError(msg) if isinstance(fp, str): fp = open(fp, "w") fp.write("# Palette\n") @@ -263,6 +268,7 @@ def load(filename): # traceback.print_exc() pass else: - raise OSError("cannot load palette") + msg = "cannot load palette" + raise OSError(msg) return lut # data, rawmode diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a34678c78..ad607a97b 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -179,7 +179,8 @@ def _toqclass_helper(im): else: if exclusive_fp: im.close() - raise ValueError(f"unsupported image mode {repr(im.mode)}") + msg = f"unsupported image mode {repr(im.mode)}" + raise ValueError(msg) size = im.size __data = data or align8to32(im.tobytes(), size[0], im.mode) diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 9df910a43..c4bb6334a 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -30,7 +30,8 @@ class Iterator: def __init__(self, im): if not hasattr(im, "seek"): - raise AttributeError("im must have seek method") + msg = "im must have seek method" + raise AttributeError(msg) self.im = im self.position = getattr(self.im, "_min_frame", 0) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 76f42a307..29d900bef 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -124,8 +124,9 @@ class Viewer: deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") - os.system(self.get_command(path, **options)) + msg = "Missing required argument: 'path'" + raise TypeError(msg) + os.system(self.get_command(path, **options)) # nosec return 1 @@ -176,7 +177,8 @@ class MacViewer(Viewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -226,7 +228,8 @@ class XDGViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["xdg-open", path]) return 1 @@ -255,7 +258,8 @@ class DisplayViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) args = ["display"] title = options.get("title") if title: @@ -286,7 +290,8 @@ class GmDisplayViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["gm", "display", path]) return 1 @@ -311,7 +316,8 @@ class EogViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) subprocess.Popen(["eog", "-n", path]) return 1 @@ -342,7 +348,8 @@ class XVViewer(UnixViewer): deprecate("The 'file' argument", 10, "'path'") path = options.pop("file") else: - raise TypeError("Missing required argument: 'path'") + msg = "Missing required argument: 'path'" + raise TypeError(msg) args = ["xv"] title = options.get("title") if title: diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 1baef7db4..b7ebddf06 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -36,7 +36,8 @@ class Stat: except AttributeError: self.h = image_or_list # assume it to be a histogram list if not isinstance(self.h, list): - raise TypeError("first argument must be image or list") + msg = "first argument must be image or list" + raise TypeError(msg) self.bands = list(range(len(self.h) // 256)) def __getattr__(self, id): diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 949cf1fbf..09a6356fa 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -284,7 +284,8 @@ def _show(image, title): super().__init__(master, image=self.image, bg="black", bd=0) if not tkinter._default_root: - raise OSError("tkinter not initialized") + msg = "tkinter not initialized" + raise OSError(msg) top = tkinter.Toplevel() if title: top.title(title) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index dc7078012..cfeadd53c 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -41,7 +41,8 @@ class ImtImageFile(ImageFile.ImageFile): buffer = self.fp.read(100) if b"\n" not in buffer: - raise SyntaxError("not an IM file") + msg = "not an IM file" + raise SyntaxError(msg) xsize = ysize = 0 diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 0bbe50668..774817569 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -66,12 +66,14 @@ class IptcImageFile(ImageFile.ImageFile): # syntax if s[0] != 0x1C or tag[0] < 1 or tag[0] > 9: - raise SyntaxError("invalid IPTC/NAA file") + msg = "invalid IPTC/NAA file" + raise SyntaxError(msg) # field size size = s[3] if size > 132: - raise OSError("illegal field length in IPTC/NAA file") + msg = "illegal field length in IPTC/NAA file" + raise OSError(msg) elif size == 128: size = 0 elif size > 128: @@ -122,7 +124,8 @@ class IptcImageFile(ImageFile.ImageFile): try: compression = COMPRESSION[self.getint((3, 120))] except KeyError as e: - raise OSError("Unknown IPTC image compression") from e + msg = "Unknown IPTC image compression" + raise OSError(msg) from e # tile if tag == (8, 10): diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index c67d8d6bf..7457874c1 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -44,13 +44,13 @@ class BoxReader: def _read_bytes(self, num_bytes): if not self._can_read(num_bytes): - raise SyntaxError("Not enough data in header") + msg = "Not enough data in header" + raise SyntaxError(msg) data = self.fp.read(num_bytes) if len(data) < num_bytes: - raise OSError( - f"Expected to read {num_bytes} bytes but only got {len(data)}." - ) + msg = f"Expected to read {num_bytes} bytes but only got {len(data)}." + raise OSError(msg) if self.remaining_in_box > 0: self.remaining_in_box -= num_bytes @@ -87,7 +87,8 @@ class BoxReader: hlen = 8 if lbox < hlen or not self._can_read(lbox - hlen): - raise SyntaxError("Invalid header length") + msg = "Invalid header length" + raise SyntaxError(msg) self.remaining_in_box = lbox - hlen return tbox @@ -189,7 +190,8 @@ def _parse_jp2_header(fp): break if size is None or mode is None: - raise SyntaxError("Malformed JP2 header") + msg = "Malformed JP2 header" + raise SyntaxError(msg) return size, mode, mimetype, dpi @@ -217,10 +219,12 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if dpi is not None: self.info["dpi"] = dpi else: - raise SyntaxError("not a JPEG 2000 file") + msg = "not a JPEG 2000 file" + raise SyntaxError(msg) if self.size is None or self.mode is None: - raise SyntaxError("unable to determine size/mode") + msg = "unable to determine size/mode" + raise SyntaxError(msg) self._reduce = 0 self.layers = 0 @@ -312,7 +316,8 @@ def _save(im, fp, filename): ] ) ): - raise ValueError("quality_layers must be a sequence of numbers") + msg = "quality_layers must be a sequence of numbers" + raise ValueError(msg) num_resolutions = info.get("num_resolutions", 0) cblk_size = info.get("codeblock_size", None) @@ -321,6 +326,7 @@ def _save(im, fp, filename): progression = info.get("progression", "LRCP") cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) + signed = info.get("signed", False) fd = -1 if hasattr(fp, "fileno"): @@ -342,6 +348,7 @@ def _save(im, fp, filename): progression, cinema_mode, mct, + signed, fd, ) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a6ed223bc..9657ae9d0 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -45,6 +45,7 @@ from . import Image, ImageFile, TiffImagePlugin from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 +from ._binary import o16be as o16 from ._deprecate import deprecate from .JpegPresets import presets @@ -89,6 +90,7 @@ def APP(self, marker): if "exif" not in self.info: # extract EXIF information (incomplete) self.info["exif"] = s # FIXME: value will change + self._exif_offset = self.fp.tell() - n + 6 elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change @@ -202,7 +204,8 @@ def SOF(self, marker): self.bits = s[0] if self.bits != 8: - raise SyntaxError(f"cannot handle {self.bits}-bit layers") + msg = f"cannot handle {self.bits}-bit layers" + raise SyntaxError(msg) self.layers = s[5] if self.layers == 1: @@ -212,7 +215,8 @@ def SOF(self, marker): elif self.layers == 4: self.mode = "CMYK" else: - raise SyntaxError(f"cannot handle {self.layers}-layer images") + msg = f"cannot handle {self.layers}-layer images" + raise SyntaxError(msg) if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: self.info["progressive"] = self.info["progression"] = 1 @@ -251,7 +255,8 @@ def DQT(self, marker): precision = 1 if (v // 16 == 0) else 2 # in bytes qt_length = 1 + precision * 64 if len(s) < qt_length: - raise SyntaxError("bad quantization table marker") + msg = "bad quantization table marker" + raise SyntaxError(msg) data = array.array("B" if precision == 1 else "H", s[1:qt_length]) if sys.byteorder == "little" and precision > 1: data.byteswap() # the values are always big-endian @@ -348,7 +353,8 @@ class JpegImageFile(ImageFile.ImageFile): s = self.fp.read(3) if not _accept(s): - raise SyntaxError("not a JPEG file") + msg = "not a JPEG file" + raise SyntaxError(msg) s = b"\xFF" # Create attributes @@ -392,7 +398,8 @@ class JpegImageFile(ImageFile.ImageFile): elif i == 0xFF00: # Skip extraneous data (escaped 0xFF) s = self.fp.read(1) else: - raise SyntaxError("no marker found") + msg = "no marker found" + raise SyntaxError(msg) def load_read(self, read_bytes): """ @@ -456,7 +463,8 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: - raise ValueError("Invalid Filename") + msg = "Invalid Filename" + raise ValueError(msg) try: with Image.open(path) as _im: @@ -522,12 +530,14 @@ def _getmp(self): info.load(file_contents) mp = dict(info) except Exception as e: - raise SyntaxError("malformed MP Index (unreadable directory)") from e + msg = "malformed MP Index (unreadable directory)" + raise SyntaxError(msg) from e # it's an error not to have a number of images try: quant = mp[0xB001] except KeyError as e: - raise SyntaxError("malformed MP Index (no number of images)") from e + msg = "malformed MP Index (no number of images)" + raise SyntaxError(msg) from e # get MP entries mpentries = [] try: @@ -549,7 +559,8 @@ def _getmp(self): if mpentryattr["ImageDataFormat"] == 0: mpentryattr["ImageDataFormat"] = "JPEG" else: - raise SyntaxError("unsupported picture format in MPO") + msg = "unsupported picture format in MPO" + raise SyntaxError(msg) mptypemap = { 0x000000: "Undefined", 0x010001: "Large Thumbnail (VGA Equivalent)", @@ -564,7 +575,8 @@ def _getmp(self): mpentries.append(mpentry) mp[0xB002] = mpentries except KeyError as e: - raise SyntaxError("malformed MP Index (bad MP Entry)") from e + msg = "malformed MP Index (bad MP Entry)" + raise SyntaxError(msg) from e # Next we should try and parse the individual image unique ID list; # we don't because I've never seen this actually used in a real MPO # file and so can't test it. @@ -624,12 +636,14 @@ def get_sampling(im): def _save(im, fp, filename): if im.width == 0 or im.height == 0: - raise ValueError("cannot write empty image as JPEG") + msg = "cannot write empty image as JPEG" + raise ValueError(msg) try: rawmode = RAWMODE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as JPEG") from e + msg = f"cannot write mode {im.mode} as JPEG" + raise OSError(msg) from e info = im.encoderinfo @@ -649,7 +663,8 @@ def _save(im, fp, filename): subsampling = preset.get("subsampling", -1) qtables = preset.get("quantization") elif not isinstance(quality, int): - raise ValueError("Invalid quality setting") + msg = "Invalid quality setting" + raise ValueError(msg) else: if subsampling in presets: subsampling = presets[subsampling].get("subsampling", -1) @@ -668,7 +683,8 @@ def _save(im, fp, filename): subsampling = 2 elif subsampling == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) subsampling = get_sampling(im) def validate_qtables(qtables): @@ -682,7 +698,8 @@ def _save(im, fp, filename): for num in line.split("#", 1)[0].split() ] except ValueError as e: - raise ValueError("Invalid quantization table") from e + msg = "Invalid quantization table" + raise ValueError(msg) from e else: qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): @@ -693,21 +710,24 @@ def _save(im, fp, filename): elif isinstance(qtables, tuple): qtables = list(qtables) if not (0 < len(qtables) < 5): - raise ValueError("None or too many quantization tables") + msg = "None or too many quantization tables" + raise ValueError(msg) for idx, table in enumerate(qtables): try: if len(table) != 64: raise TypeError table = array.array("H", table) except TypeError as e: - raise ValueError("Invalid quantization table") from e + msg = "Invalid quantization table" + raise ValueError(msg) from e else: qtables[idx] = list(table) return qtables if qtables == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + msg = "Cannot use 'keep' when original image is not a JPEG" + raise ValueError(msg) qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) @@ -724,7 +744,7 @@ def _save(im, fp, filename): icc_profile = icc_profile[MAX_DATA_BYTES_IN_MARKER:] i = 1 for marker in markers: - size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) + size = o16(2 + ICC_OVERHEAD_LEN + len(marker)) extra += ( b"\xFF\xE2" + size @@ -735,6 +755,8 @@ def _save(im, fp, filename): ) i += 1 + comment = info.get("comment", im.info.get("comment")) + # "progressive" is the official name, but older documentation # says "progression" # FIXME: issue a warning if the wrong form is used (post-1.1.7) @@ -757,6 +779,7 @@ def _save(im, fp, filename): dpi[1], subsampling, qtables, + comment, extra, exif, ) diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index cd047fe9d..8d4d826aa 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -39,7 +39,8 @@ class McIdasImageFile(ImageFile.ImageFile): # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: - raise SyntaxError("not an McIdas area file") + msg = "not an McIdas area file" + raise SyntaxError(msg) self.area_descriptor_raw = s self.area_descriptor = w = [0] + list(struct.unpack("!64i", s)) @@ -56,7 +57,8 @@ class McIdasImageFile(ImageFile.ImageFile): mode = "I" rawmode = "I;32B" else: - raise SyntaxError("unsupported McIdas format") + msg = "unsupported McIdas format" + raise SyntaxError(msg) self.mode = mode self._size = w[10], w[9] diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index d4f6c90f7..e7e1054a3 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -47,7 +47,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: - raise SyntaxError("not an MIC file; invalid OLE file") from e + msg = "not an MIC file; invalid OLE file" + raise SyntaxError(msg) from e # find ACI subfiles with Image members (maybe not the # best way to identify MIC files, but what the... ;-) @@ -60,7 +61,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): # if we didn't find any images, this is probably not # an MIC file. if not self.images: - raise SyntaxError("not an MIC file; no image entries") + msg = "not an MIC file; no image entries" + raise SyntaxError(msg) self.frame = None self._n_frames = len(self.images) @@ -77,7 +79,8 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): try: filename = self.images[frame] except IndexError as e: - raise EOFError("no such frame") from e + msg = "no such frame" + raise EOFError(msg) from e self.fp = self.ole.openstream(filename) diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index a358dfdce..2d799d6d8 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -67,7 +67,8 @@ class MpegImageFile(ImageFile.ImageFile): s = BitStream(self.fp) if s.read(32) != 0x1B3: - raise SyntaxError("not an MPEG file") + msg = "not an MPEG file" + raise SyntaxError(msg) self.mode = "RGB" self._size = s.read(12), s.read(12) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 5bfd8efc1..b1ec2c7bc 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -22,7 +22,14 @@ import itertools import os import struct -from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin +from . import ( + ExifTags, + Image, + ImageFile, + ImageSequence, + JpegImagePlugin, + TiffImagePlugin, +) from ._binary import i16be as i16 from ._binary import o32le @@ -45,14 +52,22 @@ def _save_all(im, fp, filename): _save(im, fp, filename) return + mpf_offset = 28 offsets = [] for imSequence in itertools.chain([im], append_images): for im_frame in ImageSequence.Iterator(imSequence): if not offsets: # APP2 marker - im.encoderinfo["extra"] = ( - b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70 + im_frame.encoderinfo["extra"] = ( + b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 ) + exif = im_frame.encoderinfo.get("exif") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_frame.encoderinfo["exif"] = exif + if exif: + mpf_offset += 4 + len(exif) + JpegImagePlugin._save(im_frame, fp, filename) offsets.append(fp.tell()) else: @@ -60,6 +75,7 @@ def _save_all(im, fp, filename): offsets.append(fp.tell() - offsets[-1]) ifd = TiffImagePlugin.ImageFileDirectory_v2() + ifd[0xB000] = b"0100" ifd[0xB001] = len(offsets) mpentries = b"" @@ -71,11 +87,11 @@ def _save_all(im, fp, filename): mptype = 0x000000 # Undefined mpentries += struct.pack(" 100: - raise SyntaxError("bad palette file") + msg = "bad palette file" + raise SyntaxError(msg) v = [int(x) for x in s.split()] try: diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 700f10e3f..109aad9ab 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -138,7 +138,8 @@ def _save(im, fp, filename): bpp = im.info["bpp"] im = im.point(lambda x, maxval=(1 << bpp) - 1: maxval - (x & maxval)) else: - raise OSError(f"cannot write mode {im.mode} as Palm") + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) # we ignore the palette here im.mode = "P" @@ -154,7 +155,8 @@ def _save(im, fp, filename): else: - raise OSError(f"cannot write mode {im.mode} as Palm") + msg = f"cannot write mode {im.mode} as Palm" + raise OSError(msg) # # make sure image data is available diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 38caf5c63..5802d386a 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -35,7 +35,8 @@ class PcdImageFile(ImageFile.ImageFile): s = self.fp.read(2048) if s[:4] != b"PCD_": - raise SyntaxError("not a PCD file") + msg = "not a PCD file" + raise SyntaxError(msg) orientation = s[1538] & 3 self.tile_post_rotate = None diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 442ac70c4..ecce1b097 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -63,7 +63,8 @@ class PcfFontFile(FontFile.FontFile): magic = l32(fp.read(4)) if magic != PCF_MAGIC: - raise SyntaxError("not a PCF file") + msg = "not a PCF file" + raise SyntaxError(msg) super().__init__() @@ -186,7 +187,8 @@ class PcfFontFile(FontFile.FontFile): nbitmaps = i32(fp.read(4)) if nbitmaps != len(metrics): - raise OSError("Wrong number of bitmaps") + msg = "Wrong number of bitmaps" + raise OSError(msg) offsets = [] for i in range(nbitmaps): diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 841c18a22..3202475dc 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -54,12 +54,14 @@ class PcxImageFile(ImageFile.ImageFile): # header s = self.fp.read(128) if not _accept(s): - raise SyntaxError("not a PCX file") + msg = "not a PCX file" + raise SyntaxError(msg) # image bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1 if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: - raise SyntaxError("bad PCX image size") + msg = "bad PCX image size" + raise SyntaxError(msg) logger.debug("BBox: %s %s %s %s", *bbox) # format @@ -105,7 +107,8 @@ class PcxImageFile(ImageFile.ImageFile): rawmode = "RGB;L" else: - raise OSError("unknown PCX mode") + msg = "unknown PCX mode" + raise OSError(msg) self.mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] @@ -144,7 +147,8 @@ def _save(im, fp, filename): try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: - raise ValueError(f"Cannot save {im.mode} images as PCX") from e + msg = f"Cannot save {im.mode} images as PCX" + raise ValueError(msg) from e # bytes per plane stride = (im.size[0] * bits + 7) // 8 diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 404759a7f..baad4939f 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -174,7 +174,8 @@ def _save(im, fp, filename, save_all=False): procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] else: - raise ValueError(f"cannot save mode {im.mode}") + msg = f"cannot save mode {im.mode}" + raise ValueError(msg) # # image @@ -198,7 +199,8 @@ def _save(im, fp, filename, save_all=False): elif filter == "RunLengthDecode": ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) else: - raise ValueError(f"unsupported PDF filter ({filter})") + msg = f"unsupported PDF filter ({filter})" + raise ValueError(msg) stream = op.getvalue() if filter == "CCITTFaxDecode": diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index fd5cc5a61..aa5ea2fbb 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -138,9 +138,10 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - raise IndexError( + msg = ( "object ID " + str(key) + " cannot be deleted because it doesn't exist" ) + raise IndexError(msg) def __contains__(self, key): return key in self.existing_entries or key in self.new_entries @@ -314,9 +315,8 @@ class PdfStream: expected_length = self.dictionary.Length return zlib.decompress(self.buf, bufsize=int(expected_length)) else: - raise NotImplementedError( - f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" - ) + msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" + raise NotImplementedError(msg) def pdf_repr(x): @@ -358,7 +358,8 @@ class PdfParser: def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): if buf and f: - raise RuntimeError("specify buf or f or filename, but not both buf and f") + msg = "specify buf or f or filename, but not both buf and f" + raise RuntimeError(msg) self.filename = filename self.buf = buf self.f = f @@ -816,10 +817,10 @@ class PdfParser: try: stream_len = int(result[b"Length"]) except (TypeError, KeyError, ValueError) as e: - raise PdfFormatError( - "bad or missing Length in stream dict (%r)" - % result.get(b"Length", None) - ) from e + msg = "bad or missing Length in stream dict (%r)" % result.get( + b"Length", None + ) + raise PdfFormatError(msg) from e stream_data = data[m.end() : m.end() + stream_len] m = cls.re_stream_end.match(data, m.end() + stream_len) check_format_condition(m, "stream end not found") @@ -873,7 +874,8 @@ class PdfParser: if m: return cls.get_literal_string(data, m.end()) # return None, offset # fallback (only for debugging) - raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32])) + msg = "unrecognized object: " + repr(data[offset : offset + 32]) + raise PdfFormatError(msg) re_lit_str_token = re.compile( rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" @@ -920,7 +922,8 @@ class PdfParser: result.extend(b")") nesting_depth -= 1 offset = m.end() - raise PdfFormatError("unfinished literal string") + msg = "unfinished literal string" + raise PdfFormatError(msg) re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) re_xref_subsection_start = re.compile( diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index c4860b6c4..8d0a34dba 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -44,7 +44,8 @@ class PixarImageFile(ImageFile.ImageFile): # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): - raise SyntaxError("not a PIXAR file") + msg = "not a PIXAR file" + raise SyntaxError(msg) # read rest of header s = s + self.fp.read(508) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 7fb468877..b6626bbc5 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -138,14 +138,16 @@ def __getattr__(name): if name in enum.__members__: deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") return enum[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) if dobj.unconsumed_tail: - raise ValueError("Decompressed Data Too Large") + msg = "Decompressed Data Too Large" + raise ValueError(msg) return plaintext @@ -178,7 +180,8 @@ class ChunkStream: if not is_cid(cid): if not ImageFile.LOAD_TRUNCATED_IMAGES: - raise SyntaxError(f"broken PNG file (chunk {repr(cid)})") + msg = f"broken PNG file (chunk {repr(cid)})" + raise SyntaxError(msg) return cid, pos, length @@ -215,13 +218,11 @@ class ChunkStream: crc1 = _crc32(data, _crc32(cid)) crc2 = i32(self.fp.read(4)) if crc1 != crc2: - raise SyntaxError( - f"broken PNG file (bad header checksum in {repr(cid)})" - ) + msg = f"broken PNG file (bad header checksum in {repr(cid)})" + raise SyntaxError(msg) except struct.error as e: - raise SyntaxError( - f"broken PNG file (incomplete checksum in {repr(cid)})" - ) from e + msg = f"broken PNG file (incomplete checksum in {repr(cid)})" + raise SyntaxError(msg) from e def crc_skip(self, cid, data): """Read checksum""" @@ -239,7 +240,8 @@ class ChunkStream: try: cid, pos, length = self.read() except struct.error as e: - raise OSError("truncated PNG file") from e + msg = "truncated PNG file" + raise OSError(msg) from e if cid == endchunk: break @@ -376,10 +378,11 @@ class PngStream(ChunkStream): def check_text_memory(self, chunklen): self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: - raise ValueError( + msg = ( "Too much memory used in text chunks: " f"{self.text_memory}>MAX_TEXT_MEMORY" ) + raise ValueError(msg) def save_rewind(self): self.rewind_state = { @@ -407,7 +410,8 @@ class PngStream(ChunkStream): logger.debug("Compression method %s", s[i]) comp_method = s[i] if comp_method != 0: - raise SyntaxError(f"Unknown compression method {comp_method} in iCCP chunk") + msg = f"Unknown compression method {comp_method} in iCCP chunk" + raise SyntaxError(msg) try: icc_profile = _safe_zlib_decompress(s[i + 2 :]) except ValueError: @@ -427,7 +431,8 @@ class PngStream(ChunkStream): if length < 13: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated IHDR chunk") + msg = "Truncated IHDR chunk" + raise ValueError(msg) self.im_size = i32(s, 0), i32(s, 4) try: self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])] @@ -436,7 +441,8 @@ class PngStream(ChunkStream): if s[12]: self.im_info["interlace"] = 1 if s[11]: - raise SyntaxError("unknown filter category") + msg = "unknown filter category" + raise SyntaxError(msg) return s def chunk_IDAT(self, pos, length): @@ -512,7 +518,8 @@ class PngStream(ChunkStream): if length < 1: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated sRGB chunk") + msg = "Truncated sRGB chunk" + raise ValueError(msg) self.im_info["srgb"] = s[0] return s @@ -523,7 +530,8 @@ class PngStream(ChunkStream): if length < 9: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("Truncated pHYs chunk") + msg = "Truncated pHYs chunk" + raise ValueError(msg) px, py = i32(s, 0), i32(s, 4) unit = s[8] if unit == 1: # meter @@ -567,7 +575,8 @@ class PngStream(ChunkStream): else: comp_method = 0 if comp_method != 0: - raise SyntaxError(f"Unknown compression method {comp_method} in zTXt chunk") + msg = f"Unknown compression method {comp_method} in zTXt chunk" + raise SyntaxError(msg) try: v = _safe_zlib_decompress(v[1:]) except ValueError: @@ -639,7 +648,8 @@ class PngStream(ChunkStream): if length < 8: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("APNG contains truncated acTL chunk") + msg = "APNG contains truncated acTL chunk" + raise ValueError(msg) if self.im_n_frames is not None: self.im_n_frames = None warnings.warn("Invalid APNG, will use default PNG image if possible") @@ -658,18 +668,21 @@ class PngStream(ChunkStream): if length < 26: if ImageFile.LOAD_TRUNCATED_IMAGES: return s - raise ValueError("APNG contains truncated fcTL chunk") + msg = "APNG contains truncated fcTL chunk" + raise ValueError(msg) seq = i32(s) if (self._seq_num is None and seq != 0) or ( self._seq_num is not None and self._seq_num != seq - 1 ): - raise SyntaxError("APNG contains frame sequence errors") + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) self._seq_num = seq width, height = i32(s, 4), i32(s, 8) px, py = i32(s, 12), i32(s, 16) im_w, im_h = self.im_size if px + width > im_w or py + height > im_h: - raise SyntaxError("APNG contains invalid frames") + msg = "APNG contains invalid frames" + raise SyntaxError(msg) self.im_info["bbox"] = (px, py, px + width, py + height) delay_num, delay_den = i16(s, 20), i16(s, 22) if delay_den == 0: @@ -684,11 +697,13 @@ class PngStream(ChunkStream): if ImageFile.LOAD_TRUNCATED_IMAGES: s = ImageFile._safe_read(self.fp, length) return s - raise ValueError("APNG contains truncated fDAT chunk") + msg = "APNG contains truncated fDAT chunk" + raise ValueError(msg) s = ImageFile._safe_read(self.fp, 4) seq = i32(s) if self._seq_num != seq - 1: - raise SyntaxError("APNG contains frame sequence errors") + msg = "APNG contains frame sequence errors" + raise SyntaxError(msg) self._seq_num = seq return self.chunk_IDAT(pos + 4, length - 4) @@ -713,7 +728,8 @@ class PngImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(8)): - raise SyntaxError("not a PNG file") + msg = "not a PNG file" + raise SyntaxError(msg) self._fp = self.fp self.__frame = 0 @@ -797,7 +813,8 @@ class PngImageFile(ImageFile.ImageFile): """Verify PNG file""" if self.fp is None: - raise RuntimeError("verify must be called directly after open") + msg = "verify must be called directly after open" + raise RuntimeError(msg) # back up to beginning of IDAT block self.fp.seek(self.tile[0][2] - 8) @@ -821,7 +838,8 @@ class PngImageFile(ImageFile.ImageFile): self._seek(f) except EOFError as e: self.seek(last_frame) - raise EOFError("no more images in APNG file") from e + msg = "no more images in APNG file" + raise EOFError(msg) from e def _seek(self, frame, rewind=False): if frame == 0: @@ -844,7 +862,8 @@ class PngImageFile(ImageFile.ImageFile): self.__frame = 0 else: if frame != self.__frame + 1: - raise ValueError(f"cannot seek to frame {frame}") + msg = f"cannot seek to frame {frame}" + raise ValueError(msg) # ensure previous frame was loaded self.load() @@ -869,11 +888,13 @@ class PngImageFile(ImageFile.ImageFile): break if cid == b"IEND": - raise EOFError("No more images in APNG file") + msg = "No more images in APNG file" + raise EOFError(msg) if cid == b"fcTL": if frame_start: # there must be at least one fdAT chunk between fcTL chunks - raise SyntaxError("APNG missing frame data") + msg = "APNG missing frame data" + raise SyntaxError(msg) frame_start = True try: @@ -1128,7 +1149,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) prev_disposal = Disposal.OP_BACKGROUND if prev_disposal == Disposal.OP_BACKGROUND: - base_im = previous["im"] + base_im = previous["im"].copy() dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) bbox = previous["bbox"] if bbox: @@ -1277,7 +1298,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): try: rawmode, mode = _OUTMODES[mode] except KeyError as e: - raise OSError(f"cannot write mode {mode} as PNG") from e + msg = f"cannot write mode {mode} as PNG" + raise OSError(msg) from e # # write minimal PNG file @@ -1358,7 +1380,8 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): if "transparency" in im.encoderinfo: # don't bother with transparency if it's an RGBA # and it's in the info dict. It's probably just stale. - raise OSError("cannot use transparency for this mode") + msg = "cannot use transparency for this mode" + raise OSError(msg) else: if im.mode == "P" and im.im.getpalettemode() == "RGBA": alpha = im.im.getpalette("RGBA", "A") @@ -1383,7 +1406,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): chunks.remove(cid) chunk(fp, cid, data) - exif = im.encoderinfo.get("exif", im.info.get("exif")) + exif = im.encoderinfo.get("exif") if exif: if isinstance(exif, Image.Exif): exif = exif.tobytes(8) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 392771d3e..dee2f1e15 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -84,9 +84,11 @@ class PpmImageFile(ImageFile.ImageFile): token += c if not token: # Token was not even 1 byte - raise ValueError("Reached EOF while reading header") + msg = "Reached EOF while reading header" + raise ValueError(msg) elif len(token) > 10: - raise ValueError(f"Token too long in file header: {token.decode()}") + msg = f"Token too long in file header: {token.decode()}" + raise ValueError(msg) return token def _open(self): @@ -94,7 +96,8 @@ class PpmImageFile(ImageFile.ImageFile): try: mode = MODES[magic_number] except KeyError: - raise SyntaxError("not a PPM file") + msg = "not a PPM file" + raise SyntaxError(msg) if magic_number in (b"P1", b"P4"): self.custom_mimetype = "image/x-portable-bitmap" @@ -122,9 +125,8 @@ class PpmImageFile(ImageFile.ImageFile): elif ix == 2: # token is maxval maxval = token if not 0 < maxval < 65536: - raise ValueError( - "maxval must be greater than 0 and less than 65536" - ) + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) if maxval > 255 and mode == "L": self.mode = "I" @@ -208,7 +210,8 @@ class PpmPlainDecoder(ImageFile.PyDecoder): tokens = b"".join(block.split()) for token in tokens: if token not in (48, 49): - raise ValueError(f"Invalid token for this mode: {bytes([token])}") + msg = b"Invalid token for this mode: %s" % bytes([token]) + raise ValueError(msg) data = (data + tokens)[:total_bytes] invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) @@ -241,18 +244,19 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if block and not block[-1:].isspace(): # block might split token half_token = tokens.pop() # save half token for later if len(half_token) > max_len: # prevent buildup of half_token - raise ValueError( - f"Token too long found in data: {half_token[:max_len + 1]}" + msg = ( + b"Token too long found in data: %s" % half_token[: max_len + 1] ) + raise ValueError(msg) for token in tokens: if len(token) > max_len: - raise ValueError( - f"Token too long found in data: {token[:max_len + 1]}" - ) + msg = b"Token too long found in data: %s" % token[: max_len + 1] + raise ValueError(msg) value = int(token) if value > maxval: - raise ValueError(f"Channel value too large for this mode: {value}") + msg = f"Channel value too large for this mode: {value}" + raise ValueError(msg) value = round(value / maxval * out_max) data += o32(value) if self.mode == "I" else o8(value) if len(data) == total_bytes: # finished! @@ -312,7 +316,8 @@ def _save(im, fp, filename): elif im.mode in ("RGB", "RGBA"): rawmode, head = "RGB", b"P6" else: - raise OSError(f"cannot write mode {im.mode} as PPM") + msg = f"cannot write mode {im.mode} as PPM" + raise OSError(msg) fp.write(head + b"\n%d %d\n" % im.size) if head == b"P6": fp.write(b"255\n") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index bd10e3b95..c1ca30a03 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -65,7 +65,8 @@ class PsdImageFile(ImageFile.ImageFile): s = read(26) if not _accept(s) or i16(s, 4) != 1: - raise SyntaxError("not a PSD file") + msg = "not a PSD file" + raise SyntaxError(msg) psd_bits = i16(s, 22) psd_channels = i16(s, 12) @@ -74,7 +75,8 @@ class PsdImageFile(ImageFile.ImageFile): mode, channels = MODES[(psd_mode, psd_bits)] if channels > psd_channels: - raise OSError("not enough channels") + msg = "not enough channels" + raise OSError(msg) if mode == "RGB" and psd_channels == 4: mode = "RGBA" channels = 4 @@ -152,7 +154,8 @@ class PsdImageFile(ImageFile.ImageFile): self.fp = self._fp return name, bbox except IndexError as e: - raise EOFError("no such layer") from e + msg = "no such layer" + raise EOFError(msg) from e def tell(self): # return layer number (0=image, 1..max=layers) @@ -170,7 +173,8 @@ def _layerinfo(fp, ct_bytes): # sanity check if ct_bytes < (abs(ct) * 20): - raise SyntaxError("Layer block too short for number of layers requested") + msg = "Layer block too short for number of layers requested" + raise SyntaxError(msg) for _ in range(abs(ct)): diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 9a2ec48fc..e9cb34ced 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -13,8 +13,7 @@ # Notes: # -# * Implements the pixel access object following Access. -# * Does not implement the line functions, as they don't appear to be used +# * Implements the pixel access object following Access.c # * Taking only the tuple form, which is used from python. # * Fill.c uses the integer form, but it's still going to use the old # Access.c implementation. @@ -80,7 +79,8 @@ class PyAccess: :param color: The pixel value. """ if self.readonly: - raise ValueError("Attempt to putpixel a read only image") + msg = "Attempt to putpixel a read only image" + raise ValueError(msg) (x, y) = xy if x < 0: x = self.xsize + x @@ -128,7 +128,8 @@ class PyAccess: def check_xy(self, xy): (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): - raise ValueError("pixel location out of range") + msg = "pixel location out of range" + raise ValueError(msg) return xy diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index f0207bb77..d533c55e5 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -60,7 +60,8 @@ class SgiImageFile(ImageFile.ImageFile): s = self.fp.read(headlen) if not _accept(s): - raise ValueError("Not an SGI image file") + msg = "Not an SGI image file" + raise ValueError(msg) # compression : verbatim or RLE compression = s[2] @@ -91,7 +92,8 @@ class SgiImageFile(ImageFile.ImageFile): pass if rawmode == "": - raise ValueError("Unsupported SGI image mode") + msg = "Unsupported SGI image mode" + raise ValueError(msg) self._size = xsize, ysize self.mode = rawmode.split(";")[0] @@ -124,7 +126,8 @@ class SgiImageFile(ImageFile.ImageFile): def _save(im, fp, filename): if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": - raise ValueError("Unsupported SGI image mode") + msg = "Unsupported SGI image mode" + raise ValueError(msg) # Get the keyword arguments info = im.encoderinfo @@ -133,7 +136,8 @@ def _save(im, fp, filename): bpc = info.get("bpc", 1) if bpc not in (1, 2): - raise ValueError("Unsupported number of bytes per pixel") + msg = "Unsupported number of bytes per pixel" + raise ValueError(msg) # Flip the image, since the origin of SGI file is the bottom-left corner orientation = -1 @@ -158,9 +162,8 @@ def _save(im, fp, filename): # assert we've got the right number of bands. if len(im.getbands()) != z: - raise ValueError( - f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" - ) + msg = f"incorrect number of bands in SGI write: {z} vs {len(im.getbands())}" + raise ValueError(msg) # Minimum Byte value pinmin = 0 diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index acafc320e..1192c2d73 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -110,14 +110,17 @@ class SpiderImageFile(ImageFile.ImageFile): t = struct.unpack("<27f", f) # little-endian hdrlen = isSpiderHeader(t) if hdrlen == 0: - raise SyntaxError("not a valid Spider file") + msg = "not a valid Spider file" + raise SyntaxError(msg) except struct.error as e: - raise SyntaxError("not a valid Spider file") from e + msg = "not a valid Spider file" + raise SyntaxError(msg) from e h = (99,) + t # add 1 value : spider header index starts at 1 iform = int(h[5]) if iform != 1: - raise SyntaxError("not a Spider 2D image") + msg = "not a Spider 2D image" + raise SyntaxError(msg) self._size = int(h[12]), int(h[2]) # size in pixels (width, height) self.istack = int(h[24]) @@ -140,7 +143,8 @@ class SpiderImageFile(ImageFile.ImageFile): offset = hdrlen + self.stkoffset self.istack = 2 # So Image knows it's still a stack else: - raise SyntaxError("inconsistent stack header values") + msg = "inconsistent stack header values" + raise SyntaxError(msg) if self.bigendian: self.rawmode = "F;32BF" @@ -168,7 +172,8 @@ class SpiderImageFile(ImageFile.ImageFile): def seek(self, frame): if self.istack == 0: - raise EOFError("attempt to seek in a non-stack file") + msg = "attempt to seek in a non-stack file" + raise EOFError(msg) if not self._seek_check(frame): return self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) @@ -260,7 +265,8 @@ def _save(im, fp, filename): hdr = makeSpiderHeader(im) if len(hdr) < 256: - raise OSError("Error creating Spider header") + msg = "Error creating Spider header" + raise OSError(msg) # write the SPIDER header fp.writelines(hdr) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c03759a01..c64de4444 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -54,7 +54,8 @@ class SunImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(32) if not _accept(s): - raise SyntaxError("not an SUN raster file") + msg = "not an SUN raster file" + raise SyntaxError(msg) offset = 32 @@ -83,14 +84,17 @@ class SunImageFile(ImageFile.ImageFile): else: self.mode, rawmode = "RGB", "BGRX" else: - raise SyntaxError("Unsupported Mode/Bit Depth") + msg = "Unsupported Mode/Bit Depth" + raise SyntaxError(msg) if palette_length: if palette_length > 1024: - raise SyntaxError("Unsupported Color Palette Length") + msg = "Unsupported Color Palette Length" + raise SyntaxError(msg) if palette_type != 1: - raise SyntaxError("Unsupported Palette Type") + msg = "Unsupported Palette Type" + raise SyntaxError(msg) offset = offset + palette_length self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) @@ -124,7 +128,8 @@ class SunImageFile(ImageFile.ImageFile): elif file_type == 2: self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)] else: - raise SyntaxError("Unsupported Sun Raster file type") + msg = "Unsupported Sun Raster file type" + raise SyntaxError(msg) # diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index d108362fc..20e8a083f 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -35,12 +35,14 @@ class TarIO(ContainerIO.ContainerIO): s = self.fh.read(512) if len(s) != 512: - raise OSError("unexpected end of tar file") + msg = "unexpected end of tar file" + raise OSError(msg) name = s[:100].decode("utf-8") i = name.find("\0") if i == 0: - raise OSError("cannot find subfile") + msg = "cannot find subfile" + raise OSError(msg) if i > 0: name = name[:i] diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index cd454b755..53fe6ef5c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -73,7 +73,8 @@ class TgaImageFile(ImageFile.ImageFile): or self.size[1] <= 0 or depth not in (1, 8, 16, 24, 32) ): - raise SyntaxError("not a TGA file") + msg = "not a TGA file" + raise SyntaxError(msg) # image mode if imagetype in (3, 11): @@ -89,7 +90,8 @@ class TgaImageFile(ImageFile.ImageFile): if depth == 32: self.mode = "RGBA" else: - raise SyntaxError("unknown TGA mode") + msg = "unknown TGA mode" + raise SyntaxError(msg) # orientation orientation = flags & 0x30 @@ -99,7 +101,8 @@ class TgaImageFile(ImageFile.ImageFile): elif orientation in [0, 0x10]: orientation = -1 else: - raise SyntaxError("unknown TGA orientation") + msg = "unknown TGA orientation" + raise SyntaxError(msg) self.info["orientation"] = orientation @@ -175,7 +178,8 @@ def _save(im, fp, filename): try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as TGA") from e + msg = f"cannot write mode {im.mode} as TGA" + raise OSError(msg) from e if "rle" in im.encoderinfo: rle = im.encoderinfo["rle"] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 04a63bd2b..431edfd9b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -257,6 +257,8 @@ OPEN_INFO = { (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys()) + PREFIXES = [ b"MM\x00\x2A", # Valid TIFF header with big-endian byte order b"II\x2A\x00", # Valid TIFF header with little-endian byte order @@ -498,14 +500,16 @@ class ImageFileDirectory_v2(MutableMapping): :param prefix: Override the endianness of the file. """ if not _accept(ifh): - raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") + msg = f"not a TIFF file (header {repr(ifh)} not valid)" + raise SyntaxError(msg) self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: self._endian = ">" elif self._prefix == II: self._endian = "<" else: - raise SyntaxError("not a TIFF IFD") + msg = "not a TIFF IFD" + raise SyntaxError(msg) self._bigtiff = ifh[2] == 43 self.group = group self.tagtype = {} @@ -522,7 +526,8 @@ class ImageFileDirectory_v2(MutableMapping): @legacy_api.setter def legacy_api(self, value): - raise Exception("Not allowing setting of legacy api") + msg = "Not allowing setting of legacy api" + raise Exception(msg) def reset(self): self._tags_v1 = {} # will remain empty if legacy_api is false @@ -717,6 +722,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): + if isinstance(data, int): + data = bytes((data,)) return data @_register_loader(2, 1) @@ -728,6 +735,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if isinstance(value, int): + value = str(value) if not isinstance(value, bytes): value = value.encode("ascii", "replace") return value + b"\0" @@ -774,10 +783,11 @@ class ImageFileDirectory_v2(MutableMapping): def _ensure_read(self, fp, size): ret = fp.read(size) if len(ret) != size: - raise OSError( + msg = ( "Corrupt EXIF data. " f"Expecting to read {size} bytes but only got {len(ret)}. " ) + raise OSError(msg) return ret def load(self, fp): @@ -904,7 +914,8 @@ class ImageFileDirectory_v2(MutableMapping): if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - raise NotImplementedError("multistrip support not yet implemented") + msg = "multistrip support not yet implemented" + raise NotImplementedError(msg) value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data @@ -1117,7 +1128,8 @@ class TiffImageFile(ImageFile.ImageFile): while len(self._frame_pos) <= frame: if not self.__next: - raise EOFError("no more images in TIFF file") + msg = "no more images in TIFF file" + raise EOFError(msg) logger.debug( f"Seeking to frame {frame}, on frame {self.__frame}, " f"__next {self.__next}, location: {self.fp.tell()}" @@ -1149,39 +1161,6 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame - def get_child_images(self): - if SUBIFD not in self.tag_v2: - return [] - child_images = [] - exif = self.getexif() - offset = None - for im_offset in self.tag_v2[SUBIFD]: - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - current_offset = self._fp.tell() - if offset is None: - offset = current_offset - - fp = self._fp - ifd = exif._get_ifd_dict(im_offset) - jpegInterchangeFormat = ifd.get(513) - if jpegInterchangeFormat is not None: - fp.seek(jpegInterchangeFormat) - jpeg_data = fp.read(ifd.get(514)) - - fp = io.BytesIO(jpeg_data) - - with Image.open(fp) as im: - if jpegInterchangeFormat is None: - im._frame_pos = [im_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - self._fp.seek(offset) - return child_images - def getxmp(self): """ Returns a dictionary containing the XMP tags. @@ -1257,7 +1236,8 @@ class TiffImageFile(ImageFile.ImageFile): self.load_prepare() if not len(self.tile) == 1: - raise OSError("Not exactly one tile") + msg = "Not exactly one tile" + raise OSError(msg) # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) @@ -1289,7 +1269,8 @@ class TiffImageFile(ImageFile.ImageFile): try: decoder.setimage(self.im, extents) except ValueError as e: - raise OSError("Couldn't set the image") from e + msg = "Couldn't set the image" + raise OSError(msg) from e close_self_fp = self._exclusive_fp and not self.is_animated if hasattr(self.fp, "getvalue"): @@ -1343,7 +1324,8 @@ class TiffImageFile(ImageFile.ImageFile): """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: - raise OSError("Windows Media Photo files not yet supported") + msg = "Windows Media Photo files not yet supported" + raise OSError(msg) # extract relevant tags self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] @@ -1396,6 +1378,15 @@ class TiffImageFile(ImageFile.ImageFile): SAMPLESPERPIXEL, 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, ) + + if samples_per_pixel > MAX_SAMPLESPERPIXEL: + # DOS check, samples_per_pixel can be a Long, and we extend the tuple below + logger.error( + "More samples per pixel than can be decoded: %s", samples_per_pixel + ) + msg = "Invalid value for samples per pixel" + raise SyntaxError(msg) + if samples_per_pixel < bps_actual_count: # If a file has more values in bps_tuple than expected, # remove the excess. @@ -1406,7 +1397,8 @@ class TiffImageFile(ImageFile.ImageFile): bps_tuple = bps_tuple * samples_per_pixel if len(bps_tuple) != samples_per_pixel: - raise SyntaxError("unknown data organization") + msg = "unknown data organization" + raise SyntaxError(msg) # mode: check photometric interpretation and bits per pixel key = ( @@ -1422,7 +1414,8 @@ class TiffImageFile(ImageFile.ImageFile): self.mode, rawmode = OPEN_INFO[key] except KeyError as e: logger.debug("- unsupported format") - raise SyntaxError("unknown pixel mode") from e + msg = "unknown pixel mode" + raise SyntaxError(msg) from e logger.debug(f"- raw mode: {rawmode}") logger.debug(f"- pil mode: {self.mode}") @@ -1538,7 +1531,8 @@ class TiffImageFile(ImageFile.ImageFile): layer += 1 else: logger.debug("- unsupported data organization") - raise SyntaxError("unknown data organization") + msg = "unknown data organization" + raise SyntaxError(msg) # Fix up info. if ICCPROFILE in self.tag_v2: @@ -1590,7 +1584,8 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: - raise OSError(f"cannot write mode {im.mode} as TIFF") from e + msg = f"cannot write mode {im.mode} as TIFF" + raise OSError(msg) from e ifd = ImageFileDirectory_v2(prefix=prefix) @@ -1755,11 +1750,11 @@ def _save(im, fp, filename): if "quality" in encoderinfo: quality = encoderinfo["quality"] if not isinstance(quality, int) or quality < 0 or quality > 100: - raise ValueError("Invalid quality setting") + msg = "Invalid quality setting" + raise ValueError(msg) if compression != "jpeg": - raise ValueError( - "quality setting only supported for 'jpeg' compression" - ) + msg = "quality setting only supported for 'jpeg' compression" + raise ValueError(msg) ifd[JPEGQUALITY] = quality logger.debug("Saving using libtiff encoder") @@ -1856,7 +1851,8 @@ def _save(im, fp, filename): if s: break if s < 0: - raise OSError(f"encoder error {s} when writing image file") + msg = f"encoder error {s} when writing image file" + raise OSError(msg) else: for tag in blocklist: @@ -1931,7 +1927,8 @@ class AppendingTiffWriter: elif iimm == b"MM\x00\x2a": self.setEndian(">") else: - raise RuntimeError("Invalid TIFF file header") + msg = "Invalid TIFF file header" + raise RuntimeError(msg) self.skipIFDs() self.goToEnd() @@ -1945,12 +1942,14 @@ class AppendingTiffWriter: iimm = self.f.read(4) if not iimm: - # raise RuntimeError("nothing written into new page") + # msg = "nothing written into new page" + # raise RuntimeError(msg) # Make it easy to finish a frame without committing to a new one. return if iimm != self.IIMM: - raise RuntimeError("IIMM of new page doesn't match IIMM of first page") + msg = "IIMM of new page doesn't match IIMM of first page" + raise RuntimeError(msg) ifd_offset = self.readLong() ifd_offset += self.offsetOfNewPage @@ -2024,29 +2023,34 @@ class AppendingTiffWriter: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def rewriteLastShort(self, value): self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2") + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) def rewriteLastLong(self, value): self.f.seek(-4, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def writeShort(self, value): bytes_written = self.f.write(struct.pack(self.shortFmt, value)) if bytes_written is not None and bytes_written != 2: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2") + msg = f"wrote only {bytes_written} bytes but wanted 2" + raise RuntimeError(msg) def writeLong(self, value): bytes_written = self.f.write(struct.pack(self.longFmt, value)) if bytes_written is not None and bytes_written != 4: - raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4") + msg = f"wrote only {bytes_written} bytes but wanted 4" + raise RuntimeError(msg) def close(self): self.finalize() @@ -2089,7 +2093,8 @@ class AppendingTiffWriter: def fixOffsets(self, count, isShort=False, isLong=False): if not isShort and not isLong: - raise RuntimeError("offset is neither short nor long") + msg = "offset is neither short nor long" + raise RuntimeError(msg) for i in range(count): offset = self.readShort() if isShort else self.readLong() @@ -2097,7 +2102,8 @@ class AppendingTiffWriter: if isShort and offset >= 65536: # offset is now too large - we must convert shorts to longs if count != 1: - raise RuntimeError("not implemented") # XXX TODO + msg = "not implemented" + raise RuntimeError(msg) # XXX TODO # simple case - the offset is just one and therefore it is # local (not referenced with another offset) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 5eaeb10cc..1d074f78c 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -98,6 +98,15 @@ class WebPImageFile(ImageFile.ImageFile): return None return self.getexif()._get_merged_dict() + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + + :returns: XMP tags in a dictionary. + """ + return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} + def seek(self, frame): if not self._seek_check(frame): return @@ -121,7 +130,8 @@ class WebPImageFile(ImageFile.ImageFile): if ret is None: self._reset() # Reset just to be safe self.seek(0) - raise EOFError("failed to decode next frame in WebP file") + msg = "failed to decode next frame in WebP file" + raise EOFError(msg) # Compute duration data, timestamp = ret @@ -224,9 +234,8 @@ def _save_all(im, fp, filename): or len(background) != 4 or not all(0 <= v < 256 for v in background) ): - raise OSError( - f"Background color is not an RGBA tuple clamped to (0-255): {background}" - ) + msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}" + raise OSError(msg) # Convert to packed uint bg_r, bg_g, bg_b, bg_a = background @@ -302,7 +311,8 @@ def _save_all(im, fp, filename): # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) if data is None: - raise OSError("cannot write file as WebP (encoder returned None)") + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) fp.write(data) @@ -318,6 +328,7 @@ def _save(im, fp, filename): exif = exif[6:] xmp = im.encoderinfo.get("xmp", "") method = im.encoderinfo.get("method", 4) + exact = 1 if im.encoderinfo.get("exact") else 0 if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( @@ -336,11 +347,13 @@ def _save(im, fp, filename): im.mode, icc_profile, method, + exact, exif, xmp, ) if data is None: - raise OSError("cannot write file as WebP (encoder returned None)") + msg = "cannot write file as WebP (encoder returned None)" + raise OSError(msg) fp.write(data) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 2f54cdebb..639730b8e 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -109,7 +109,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": - raise SyntaxError("Unsupported WMF file format") + msg = "Unsupported WMF file format" + raise SyntaxError(msg) elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": # enhanced metafile @@ -137,7 +138,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = xdpi, ydpi else: - raise SyntaxError("Unsupported file format") + msg = "Unsupported file format" + raise SyntaxError(msg) self.mode = "RGB" self._size = size @@ -162,7 +164,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): def _save(im, fp, filename): if _handler is None or not hasattr(_handler, "save"): - raise OSError("WMF save handler not installed") + msg = "WMF save handler not installed" + raise OSError(msg) _handler.save(im, fp, filename) diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 4efedb77e..f0e05e867 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -49,7 +49,8 @@ class XVThumbImageFile(ImageFile.ImageFile): # check magic if not _accept(self.fp.read(6)): - raise SyntaxError("not an XV thumbnail file") + msg = "not an XV thumbnail file" + raise SyntaxError(msg) # Skip to beginning of next line self.fp.readline() @@ -58,7 +59,8 @@ class XVThumbImageFile(ImageFile.ImageFile): while True: s = self.fp.readline() if not s: - raise SyntaxError("Unexpected EOF reading XV thumbnail file") + msg = "Unexpected EOF reading XV thumbnail file" + raise SyntaxError(msg) if s[0] != 35: # ie. when not a comment: '#' break diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 59acabeba..ad18e0031 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -53,7 +53,8 @@ class XbmImageFile(ImageFile.ImageFile): m = xbm_head.match(self.fp.read(512)) if not m: - raise SyntaxError("not a XBM file") + msg = "not a XBM file" + raise SyntaxError(msg) xsize = int(m.group("width")) ysize = int(m.group("height")) @@ -70,7 +71,8 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): if im.mode != "1": - raise OSError(f"cannot write mode {im.mode} as XBM") + msg = f"cannot write mode {im.mode} as XBM" + raise OSError(msg) fp.write(f"#define im_width {im.size[0]}\n".encode("ascii")) fp.write(f"#define im_height {im.size[1]}\n".encode("ascii")) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index aaed2039d..5fae4cd68 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -40,13 +40,15 @@ class XpmImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(9)): - raise SyntaxError("not an XPM file") + msg = "not an XPM file" + raise SyntaxError(msg) # skip forward to next string while True: s = self.fp.readline() if not s: - raise SyntaxError("broken XPM file") + msg = "broken XPM file" + raise SyntaxError(msg) m = xpm_head.match(s) if m: break @@ -57,7 +59,8 @@ class XpmImageFile(ImageFile.ImageFile): bpp = int(m.group(4)) if pal > 256 or bpp != 1: - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) # # load palette description @@ -91,13 +94,15 @@ class XpmImageFile(ImageFile.ImageFile): ) else: # unknown colour - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) break else: # missing colour key - raise ValueError("cannot read this XPM file") + msg = "cannot read this XPM file" + raise ValueError(msg) self.mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 30a8a8971..7c4b1623d 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -43,14 +43,17 @@ def deprecate( if when is None: removed = "a future version" elif when <= int(__version__.split(".")[0]): - raise RuntimeError(f"{deprecated} {is_} deprecated and should be removed.") + msg = f"{deprecated} {is_} deprecated and should be removed." + raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" else: - raise ValueError(f"Unknown removal version, update {__name__}?") + msg = f"Unknown removal version, update {__name__}?" + raise ValueError(msg) if replacement and action: - raise ValueError("Use only one of 'replacement' and 'action'") + msg = "Use only one of 'replacement' and 'action'" + raise ValueError(msg) if replacement: action = f". Use {replacement} instead." diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 8e736a432..1cc1d0f1c 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.3.0.dev0" +__version__ = "9.4.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 3838568f3..6f9d99e76 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -25,7 +25,8 @@ def check_module(feature): :raises ValueError: If the module is not defined in this version of Pillow. """ if not (feature in modules): - raise ValueError(f"Unknown module {feature}") + msg = f"Unknown module {feature}" + raise ValueError(msg) module, ver = modules[feature] @@ -78,7 +79,8 @@ def check_codec(feature): :raises ValueError: If the codec is not defined in this version of Pillow. """ if feature not in codecs: - raise ValueError(f"Unknown codec {feature}") + msg = f"Unknown codec {feature}" + raise ValueError(msg) codec, lib = codecs[feature] @@ -135,7 +137,8 @@ def check_feature(feature): :raises ValueError: If the feature is not defined in this version of Pillow. """ if feature not in features: - raise ValueError(f"Unknown feature {feature}") + msg = f"Unknown feature {feature}" + raise ValueError(msg) module, flag, ver = features[feature] diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index 5b3f18ace..16b9a2edd 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -364,17 +364,6 @@ load_tkinter_funcs(void) { * tkinter dynamic library (module). */ -/* From module __file__ attribute to char *string for dlopen. */ -char * -fname2char(PyObject *fname) { - PyObject *bytes; - bytes = PyUnicode_EncodeFSDefault(fname); - if (bytes == NULL) { - return NULL; - } - return PyBytes_AsString(bytes); -} - #include void * @@ -442,7 +431,7 @@ load_tkinter_funcs(void) { int ret = -1; void *main_program, *tkinter_lib; char *tkinter_libname; - PyObject *pModule = NULL, *pString = NULL; + PyObject *pModule = NULL, *pString = NULL, *pBytes = NULL; /* Try loading from the main program namespace first */ main_program = dlopen(NULL, RTLD_LAZY); @@ -462,7 +451,12 @@ load_tkinter_funcs(void) { if (pString == NULL) { goto exit; } - tkinter_libname = fname2char(pString); + /* From module __file__ attribute to char *string for dlopen. */ + pBytes = PyUnicode_EncodeFSDefault(pString); + if (pBytes == NULL) { + goto exit; + } + tkinter_libname = PyBytes_AsString(pBytes); if (tkinter_libname == NULL) { goto exit; } @@ -478,6 +472,7 @@ exit: dlclose(main_program); Py_XDECREF(pModule); Py_XDECREF(pString); + Py_XDECREF(pBytes); return ret; } #endif /* end not Windows */ diff --git a/src/_imaging.c b/src/_imaging.c index 0888188fb..05e1370f6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1531,25 +1531,21 @@ if (PySequence_Check(op)) { \ PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } + int endian = strncmp(image->mode, "I;16", 4) == 0 ? (strcmp(image->mode, "I;16B") == 0 ? 2 : 1) : 0; double value; - if (scale == 1.0 && offset == 0.0) { - /* Clipped data */ - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - image->image8[y][x] = (UINT8)CLIP8(value); - if (++x >= (int)image->xsize) { - x = 0, y++; - } + for (i = x = y = 0; i < n; i++) { + set_value_to_item(seq, i); + if (scale != 1.0 || offset != 0.0) { + value = value * scale + offset; } - - } else { - /* Scaled and clipped data */ - for (i = x = y = 0; i < n; i++) { - set_value_to_item(seq, i); - image->image8[y][x] = CLIP8(value * scale + offset); - if (++x >= (int)image->xsize) { - x = 0, y++; - } + if (endian == 0) { + image->image8[y][x] = (UINT8)CLIP8(value); + } else { + image->image8[y][x * 2 + (endian == 2 ? 1 : 0)] = CLIP8((int)value % 256); + image->image8[y][x * 2 + (endian == 2 ? 0 : 1)] = CLIP8((int)value >> 8); + } + if (++x >= (int)image->xsize) { + x = 0, y++; } } PyErr_Clear(); /* Avoid weird exceptions */ @@ -1829,7 +1825,7 @@ _resize(ImagingObject *self, PyObject *args) { box[1] - (int)box[1] == 0 && box[3] - box[1] == ysize) { imOut = ImagingCrop(imIn, box[0], box[1], box[2], box[3]); } else if (filter == IMAGING_TRANSFORM_NEAREST) { - double a[6]; + double a[8]; memset(a, 0, sizeof a); a[0] = (double)(box[2] - box[0]) / xsize; diff --git a/src/_imagingft.c b/src/_imagingft.c index 8f19b763c..b52d6353e 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -303,7 +303,7 @@ text_layout_raqm( goto failed; } - len = PySequence_Size(seq); + len = PySequence_Fast_GET_SIZE(seq); for (j = 0; j < len; j++) { PyObject *item = PySequence_Fast_GET_ITEM(seq, j); char *feature = NULL; @@ -311,23 +311,26 @@ text_layout_raqm( PyObject *bytes; if (!PyUnicode_Check(item)) { + Py_DECREF(seq); PyErr_SetString(PyExc_TypeError, "expected a string"); goto failed; } - - if (PyUnicode_Check(item)) { - bytes = PyUnicode_AsUTF8String(item); - if (bytes == NULL) { - goto failed; - } - feature = PyBytes_AS_STRING(bytes); - size = PyBytes_GET_SIZE(bytes); + bytes = PyUnicode_AsUTF8String(item); + if (bytes == NULL) { + Py_DECREF(seq); + goto failed; } + feature = PyBytes_AS_STRING(bytes); + size = PyBytes_GET_SIZE(bytes); if (!raqm_add_font_feature(rq, feature, size)) { + Py_DECREF(seq); + Py_DECREF(bytes); PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); goto failed; } + Py_DECREF(bytes); } + Py_DECREF(seq); } if (!raqm_set_freetype_face(rq, self->face)) { @@ -774,13 +777,15 @@ font_render(FontObject *self, PyObject *args) { const char *lang = NULL; PyObject *features = Py_None; PyObject *string; + float x_start = 0; + float y_start = 0; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziL:render", + "On|zzOziLff:render", &string, &id, &mode, @@ -788,7 +793,9 @@ font_render(FontObject *self, PyObject *args) { &features, &lang, &stroke_width, - &foreground_ink_long)) { + &foreground_ink_long, + &x_start, + &y_start)) { return NULL; } @@ -873,8 +880,8 @@ font_render(FontObject *self, PyObject *args) { } /* set pen position to text origin */ - x = (-x_min + stroke_width) << 6; - y = (-y_max + (-stroke_width)) << 6; + x = (-x_min + stroke_width + x_start) * 64; + y = (-y_max + (-stroke_width) - y_start) * 64; if (stroker == NULL) { load_flags |= FT_LOAD_RENDER; @@ -953,7 +960,7 @@ font_render(FontObject *self, PyObject *args) { /* we didn't ask for color, fall through to default */ #endif default: - PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; } @@ -1020,7 +1027,7 @@ font_render(FontObject *self, PyObject *args) { } } } else { - PyErr_SetString(PyExc_IOError, "unsupported bitmap pixel mode"); + PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; } } @@ -1179,7 +1186,7 @@ font_setvaraxes(FontObject *self, PyObject *args) { } num_coords = PyObject_Length(axes); - coords = malloc(2 * sizeof(coords)); + coords = (FT_Fixed*)malloc(num_coords * sizeof(FT_Fixed)); if (coords == NULL) { return PyErr_NoMemory(); } diff --git a/src/_webp.c b/src/_webp.c index fd99116cb..493e0709c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -178,12 +178,11 @@ _anim_encoder_new(PyObject *self, PyObject *args) { return NULL; } -PyObject * +void _anim_encoder_dealloc(PyObject *self) { WebPAnimEncoderObject *encp = (WebPAnimEncoderObject *)self; WebPPictureFree(&(encp->frame)); WebPAnimEncoderDelete(encp->enc); - Py_RETURN_NONE; } PyObject * @@ -400,12 +399,11 @@ _anim_decoder_new(PyObject *self, PyObject *args) { return NULL; } -PyObject * +void _anim_decoder_dealloc(PyObject *self) { WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; WebPDataClear(&(decp->data)); WebPAnimDecoderDelete(decp->dec); - Py_RETURN_NONE; } PyObject * @@ -576,6 +574,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { int lossless; float quality_factor; int method; + int exact; uint8_t *rgb; uint8_t *icc_bytes; uint8_t *exif_bytes; @@ -597,7 +596,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "y#iiifss#is#s#", + "y#iiifss#iis#s#", (char **)&rgb, &size, &width, @@ -608,6 +607,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { &icc_bytes, &icc_size, &method, + &exact, &exif_bytes, &exif_size, &xmp_bytes, @@ -633,6 +633,10 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.lossless = lossless; config.quality = quality_factor; config.method = method; +#if WEBP_ENCODER_ABI_VERSION >= 0x0209 + // the "exact" flag is only available in libwebp 0.5.0 and later + config.exact = exact; +#endif // Validate the config if (!WebPValidateConfig(&config)) { diff --git a/src/encode.c b/src/encode.c index 72c7f64d0..21c42d915 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1048,6 +1048,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { PyObject *qtables = NULL; unsigned int *qarrays = NULL; int qtablesLen = 0; + char *comment = NULL; + Py_ssize_t comment_size; char *extra = NULL; Py_ssize_t extra_size; char *rawExif = NULL; @@ -1055,7 +1057,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnOy#y#", + "ss|nnnnnnnnOz#y#y#", &mode, &rawmode, &quality, @@ -1067,6 +1069,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { &ydpi, &subsampling, &qtables, + &comment, + &comment_size, &extra, &extra_size, &rawExif, @@ -1090,13 +1094,28 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { return NULL; } - // Freed in JpegEncode, Case 5 + // Freed in JpegEncode, Case 6 qarrays = get_qtables_arrays(qtables, &qtablesLen); + if (comment && comment_size > 0) { + /* malloc check ok, length is from python parsearg */ + char *p = malloc(comment_size); // Freed in JpegEncode, Case 6 + if (!p) { + return ImagingError_MemoryError(); + } + memcpy(p, comment, comment_size); + comment = p; + } else { + comment = NULL; + } + if (extra && extra_size > 0) { /* malloc check ok, length is from python parsearg */ - char *p = malloc(extra_size); // Freed in JpegEncode, Case 5 + char *p = malloc(extra_size); // Freed in JpegEncode, Case 6 if (!p) { + if (comment) { + free(comment); + } return ImagingError_MemoryError(); } memcpy(p, extra, extra_size); @@ -1107,8 +1126,11 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (rawExif && rawExifLen > 0) { /* malloc check ok, length is from python parsearg */ - char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5 + char *pp = malloc(rawExifLen); // Freed in JpegEncode, Case 6 if (!pp) { + if (comment) { + free(comment); + } if (extra) { free(extra); } @@ -1134,6 +1156,8 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { ((JPEGENCODERSTATE *)encoder->state.context)->streamtype = streamtype; ((JPEGENCODERSTATE *)encoder->state.context)->xdpi = xdpi; ((JPEGENCODERSTATE *)encoder->state.context)->ydpi = ydpi; + ((JPEGENCODERSTATE *)encoder->state.context)->comment = comment; + ((JPEGENCODERSTATE *)encoder->state.context)->comment_size = comment_size; ((JPEGENCODERSTATE *)encoder->state.context)->extra = extra; ((JPEGENCODERSTATE *)encoder->state.context)->extra_size = extra_size; ((JPEGENCODERSTATE *)encoder->state.context)->rawExif = rawExif; @@ -1188,11 +1212,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char *cinema_mode = "no"; OPJ_CINEMA_MODE cine_mode; char mct = 0; + int sgnd = 0; Py_ssize_t fd = -1; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbn", + "ss|OOOsOnOOOssbbn", &mode, &format, &offset, @@ -1207,6 +1232,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &progression, &cinema_mode, &mct, + &sgnd, &fd)) { return NULL; } @@ -1305,6 +1331,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->progression = prog_order; context->cinema_mode = cine_mode; context->mct = mct; + context->sgnd = sgnd; return (PyObject *)encoder; } diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 514fb2929..83860c38a 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -43,23 +43,6 @@ add_item(const char *mode) { return &access_table[i]; } -/* fetch pointer to pixel line */ - -static void * -line_8(Imaging im, int x, int y) { - return &im->image8[y][x]; -} - -static void * -line_16(Imaging im, int x, int y) { - return &im->image8[y][x + x]; -} - -static void * -line_32(Imaging im, int x, int y) { - return &im->image32[y][x]; -} - /* fetch individual pixel */ static void @@ -187,36 +170,35 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { void ImagingAccessInit() { -#define ADD(mode_, line_, get_pixel_, put_pixel_) \ +#define ADD(mode_, get_pixel_, put_pixel_) \ { \ ImagingAccess access = add_item(mode_); \ - access->line = line_; \ access->get_pixel = get_pixel_; \ access->put_pixel = put_pixel_; \ } /* populate access table */ - ADD("1", line_8, get_pixel_8, put_pixel_8); - ADD("L", line_8, get_pixel_8, put_pixel_8); - ADD("LA", line_32, get_pixel, put_pixel); - ADD("La", line_32, get_pixel, put_pixel); - ADD("I", line_32, get_pixel_32, put_pixel_32); - ADD("I;16", line_16, get_pixel_16L, put_pixel_16L); - ADD("I;16L", line_16, get_pixel_16L, put_pixel_16L); - ADD("I;16B", line_16, get_pixel_16B, put_pixel_16B); - ADD("I;32L", line_32, get_pixel_32L, put_pixel_32L); - ADD("I;32B", line_32, get_pixel_32B, put_pixel_32B); - ADD("F", line_32, get_pixel_32, put_pixel_32); - ADD("P", line_8, get_pixel_8, put_pixel_8); - ADD("PA", line_32, get_pixel, put_pixel); - ADD("RGB", line_32, get_pixel_32, put_pixel_32); - ADD("RGBA", line_32, get_pixel_32, put_pixel_32); - ADD("RGBa", line_32, get_pixel_32, put_pixel_32); - ADD("RGBX", line_32, get_pixel_32, put_pixel_32); - ADD("CMYK", line_32, get_pixel_32, put_pixel_32); - ADD("YCbCr", line_32, get_pixel_32, put_pixel_32); - ADD("LAB", line_32, get_pixel_32, put_pixel_32); - ADD("HSV", line_32, get_pixel_32, put_pixel_32); + ADD("1", get_pixel_8, put_pixel_8); + ADD("L", get_pixel_8, put_pixel_8); + ADD("LA", get_pixel, put_pixel); + ADD("La", get_pixel, put_pixel); + ADD("I", get_pixel_32, put_pixel_32); + ADD("I;16", get_pixel_16L, put_pixel_16L); + ADD("I;16L", get_pixel_16L, put_pixel_16L); + ADD("I;16B", get_pixel_16B, put_pixel_16B); + ADD("I;32L", get_pixel_32L, put_pixel_32L); + ADD("I;32B", get_pixel_32B, put_pixel_32B); + ADD("F", get_pixel_32, put_pixel_32); + ADD("P", get_pixel_8, put_pixel_8); + ADD("PA", get_pixel, put_pixel); + ADD("RGB", get_pixel_32, put_pixel_32); + ADD("RGBA", get_pixel_32, put_pixel_32); + ADD("RGBa", get_pixel_32, put_pixel_32); + ADD("RGBX", get_pixel_32, put_pixel_32); + ADD("CMYK", get_pixel_32, put_pixel_32); + ADD("YCbCr", get_pixel_32, put_pixel_32); + ADD("LAB", get_pixel_32, put_pixel_32); + ADD("HSV", get_pixel_32, put_pixel_32); } ImagingAccess diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index fd6e268b5..aee7cda06 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -7,8 +7,8 @@ #define PRECISION_BITS (16 - 8 - 2) #define PRECISION_ROUNDING (1 << (PRECISION_BITS - 1)) -/* 8 — scales are multiplied on byte. - 6 — max index in the table +/* 8 - scales are multiplied on byte. + 6 - max index in the table (max size is 65, but index 64 is not reachable) */ #define SCALE_BITS (32 - 8 - 6) #define SCALE_MASK ((1 << SCALE_BITS) - 1) @@ -44,14 +44,14 @@ table_index3D(int index1D, int index2D, int index3D, int size1D, int size1D_2D) Transforms colors of imIn using provided 3D lookup table and puts the result in imOut. Returns imOut on success or 0 on error. - imOut, imIn — images, should be the same size and may be the same image. + imOut, imIn - images, should be the same size and may be the same image. Should have 3 or 4 channels. - table_channels — number of channels in the lookup table, 3 or 4. + table_channels - number of channels in the lookup table, 3 or 4. Should be less or equal than number of channels in imOut image; - size1D, size_2D and size3D — dimensions of provided table; - table — flat table, - array with table_channels × size1D × size2D × size3D elements, - where channels are changed first, then 1D, then​ 2D, then 3D. + size1D, size_2D and size3D - dimensions of provided table; + table - flat table, + array with table_channels * size1D * size2D * size3D elements, + where channels are changed first, then 1D, then 2D, then 3D. Each element is signed 16-bit int where 0 is lowest output value and 255 << PRECISION_BITS (16320) is highest value. */ diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index bdc680be4..b03bd02af 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -43,13 +43,6 @@ #define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) #define L24(rgb) ((rgb)[0] * 19595 + (rgb)[1] * 38470 + (rgb)[2] * 7471 + 0x8000) -#ifndef round -double -round(double x) { - return floor(x + 0.5); -} -#endif - /* ------------------- */ /* 1 (bit) conversions */ /* ------------------- */ @@ -486,6 +479,25 @@ rgba2rgbA(UINT8 *out, const UINT8 *in, int xsize) { } } +static void +rgba2rgb_(UINT8 *out, const UINT8 *in, int xsize) { + int x; + unsigned int alpha; + for (x = 0; x < xsize; x++, in += 4) { + alpha = in[3]; + if (alpha == 255 || alpha == 0) { + *out++ = in[0]; + *out++ = in[1]; + *out++ = in[2]; + } else { + *out++ = CLIP8((255 * in[0]) / alpha); + *out++ = CLIP8((255 * in[1]) / alpha); + *out++ = CLIP8((255 * in[2]) / alpha); + } + *out++ = 255; + } +} + /* * Conversion of RGB + single transparent color to RGBA, * where any pixel that matches the color will have the @@ -941,6 +953,7 @@ static struct { {"RGBA", "HSV", rgb2hsv}, {"RGBa", "RGBA", rgba2rgbA}, + {"RGBa", "RGB", rgba2rgb_}, {"RGBX", "1", rgb2bit}, {"RGBX", "L", rgb2l}, diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index b65f8eadd..d9ded1852 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -124,7 +124,6 @@ struct ImagingMemoryInstance { struct ImagingAccessInstance { const char *mode; - void *(*line)(Imaging im, int x, int y); void (*get_pixel)(Imaging im, int x, int y, void *pixel); void (*put_pixel)(Imaging im, int x, int y, const void *pixel); }; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index a876d3bb6..1d7550818 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -92,6 +92,10 @@ typedef struct { /* in factors of DCTSIZE2 */ int qtablesLen; + /* Comment */ + char *comment; + size_t comment_size; + /* Extra data (to be injected after header) */ char *extra; int extra_size; diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index d030b0c43..b28a0440a 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -85,6 +85,9 @@ typedef struct { /* Set multiple component transformation */ char mct; + /* Signed */ + int sgnd; + /* Progression order (LRCP/RLCP/RPCL/PCRL/CPRL) */ OPJ_PROG_ORDER progression; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index fe5511ba5..db1c5c0c9 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -343,7 +343,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { image_params[n].x0 = image_params[n].y0 = 0; image_params[n].prec = prec; image_params[n].bpp = bpp; - image_params[n].sgnd = 0; + image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; } image = opj_image_create(components, image_params, color_space); diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index a44debcaf..2a24eff39 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -277,6 +277,13 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { } case 4: + + if (context->comment) { + jpeg_write_marker(&context->cinfo, JPEG_COM, (unsigned char *)context->comment, context->comment_size); + } + state->state++; + + case 5: if (1024 > context->destination.pub.free_in_buffer) { break; } @@ -301,7 +308,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { state->state++; /* fall through */ - case 5: + case 6: /* Finish compression */ if (context->destination.pub.free_in_buffer < 100) { @@ -310,6 +317,10 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { jpeg_finish_compress(&context->cinfo); /* Clean up */ + if (context->comment) { + free(context->comment); + context->comment = NULL; + } if (context->extra) { free(context->extra); context->extra = NULL; diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index 137ed242a..182eb62a7 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -21,6 +21,7 @@ Imaging ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { Imaging imOut; int x, y; + ImagingSectionCookie cookie; /* Assume there's enough data in the buffer */ if (!im) { @@ -33,6 +34,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { return NULL; } + ImagingSectionEnter(&cookie); for (y = 0; y < im->ysize; y++) { UINT8 *in = (UINT8 *)im->image[y]; UINT8 *out = (UINT8 *)imOut->image[y]; @@ -43,6 +45,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { in += 4; } } + ImagingSectionLeave(&cookie); } else if (strlen(mode) == 3 && im->bands == 3) { imOut = ImagingNewDirty(mode, im->xsize, im->ysize); @@ -54,6 +57,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { UINT8 *in = (UINT8 *)im->image[y]; UINT8 *out = (UINT8 *)imOut->image[y]; + ImagingSectionEnter(&cookie); for (x = 0; x < im->xsize; x++) { float v0 = m[0] * in[0] + m[1] * in[1] + m[2] * in[2] + m[3] + 0.5; float v1 = m[4] * in[0] + m[5] * in[1] + m[6] * in[2] + m[7] + 0.5; @@ -64,6 +68,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { in += 4; out += 4; } + ImagingSectionLeave(&cookie); } } else { return (Imaging)ImagingError_ModeError(); diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index dfa6d842d..783852c24 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1717,7 +1717,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { withAlpha = !strcmp(im->mode, "RGBA"); int transparency = 0; - unsigned char r, g, b; + unsigned char r = 0, g = 0, b = 0; for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 7663f96a9..428cd93d2 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -771,11 +771,11 @@ ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp) { TRACE(("Opening using fd: %d for writing \n", clientstate->fp)); clientstate->tiff = TIFFFdOpen(fd_to_tiff_fd(clientstate->fp), filename, mode); } else { - // malloc a buffer to write the tif, we're going to need to realloc or something + // calloc a buffer to write the tif, we're going to need to realloc or something // if we need bigger. TRACE(("Opening a buffer for writing \n")); - /* malloc check ok, small constant allocation */ - clientstate->data = malloc(bufsize); + /* calloc check ok, small constant allocation */ + clientstate->data = calloc(bufsize, 1); clientstate->size = bufsize; clientstate->flrealloc = 1; diff --git a/tox.ini b/tox.ini index 21b5d4b50..9a41ca96b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,13 @@ -# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, -# "python3 -m pip install tox" and then run "tox" from this directory. - [tox] envlist = lint - py{37,38,39,310,311,py3} + py{py3, 311, 310, 39, 38, 37} minversion = 1.9 [testenv] +deps = + cffi + numpy extras = tests commands = @@ -17,16 +15,15 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -deps = - cffi - numpy +allowlist_externals = make [testenv:lint] +passenv = + PRE_COMMIT_COLOR +skip_install = true +deps = + check-manifest + pre-commit commands = pre-commit run --all-files --show-diff-on-failure check-manifest -deps = - pre-commit - check-manifest -skip_install = true -passenv = PRE_COMMIT_COLOR diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f4858b630..f5050946c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,5 +1,6 @@ import os import platform +import re import shutil import struct import subprocess @@ -111,6 +112,11 @@ deps = { + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download", "filename": "libjpeg-turbo-2.1.4.tar.gz", "dir": "libjpeg-turbo-2.1.4", + "license": ["README.ijg", "LICENSE.md"], + "license_pattern": ( + "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" + ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" + ), "build": [ cmd_cmake( [ @@ -135,6 +141,8 @@ deps = { "url": "https://zlib.net/zlib1213.zip", "filename": "zlib1213.zip", "dir": "zlib-1.2.13", + "license": "README", + "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ cmd_nmake(r"win32\Makefile.msc", "clean"), cmd_nmake(r"win32\Makefile.msc", "zlib.lib"), @@ -143,10 +151,69 @@ deps = { "headers": [r"z*.h"], "libs": [r"*.lib"], }, + "xz": { + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.0.tar.gz/download", + "filename": "xz-5.4.0.tar.gz", + "dir": "xz-5.4.0", + "license": "COPYING", + "patch": { + r"src\liblzma\api\lzma.h": { + "#ifndef LZMA_API_IMPORT": "#ifndef LZMA_API_IMPORT\n#define LZMA_API_STATIC", # noqa: E501 + }, + r"windows\vs2019\liblzma.vcxproj": { + # retarget to default toolset (selected by vcvarsall.bat) + "v142": "$(DefaultPlatformToolset)", # noqa: E501 + # retarget to latest (selected by vcvarsall.bat) + "10.0": "$(WindowsSDKVersion)", # noqa: E501 + }, + }, + "build": [ + cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Clean"), + cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Build"), + cmd_mkdir(r"{inc_dir}\lzma"), + cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), + ], + "headers": [r"src\liblzma\api\lzma.h"], + "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], + }, + "libwebp": { + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", + "filename": "libwebp-1.2.4.tar.gz", + "dir": "libwebp-1.2.4", + "license": "COPYING", + "build": [ + cmd_rmdir(r"output\release-static"), # clean + cmd_nmake( + "Makefile.vc", + "all", + [ + "CFG=release-static", + "RTLIBCFG=dynamic", + "OBJDIR=output", + "ARCH={architecture}", + "LIBWEBP_BASENAME=webp", + ], + ), + cmd_mkdir(r"{inc_dir}\webp"), + cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), + ], + "libs": [r"output\release-static\{architecture}\lib\*.lib"], + }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.4.0.tar.gz", - "filename": "tiff-4.4.0.tar.gz", - "dir": "tiff-4.4.0", + "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz", + "filename": "tiff-4.5.0.tar.gz", + "dir": "tiff-4.5.0", + "license": "LICENSE.md", + "patch": { + r"libtiff\tif_lzma.c": { + # link against liblzma.lib + "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 + }, + r"libtiff\tif_webp.c": { + # link against webp.lib + "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 + }, + }, "build": [ cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_nmake(target="clean"), @@ -156,26 +223,11 @@ deps = { "libs": [r"libtiff\*.lib"], # "bins": [r"libtiff\*.dll"], }, - "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.2.4.tar.gz", - "filename": "libwebp-1.2.4.tar.gz", - "dir": "libwebp-1.2.4", - "build": [ - cmd_rmdir(r"output\release-static"), # clean - cmd_nmake( - "Makefile.vc", - "all", - ["CFG=release-static", "OBJDIR=output", "ARCH={architecture}"], - ), - cmd_mkdir(r"{inc_dir}\webp"), - cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), - ], - "libs": [r"output\release-static\{architecture}\lib\*.lib"], - }, "libpng": { - "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.37/lpng1637.zip/download", - "filename": "lpng1637.zip", - "dir": "lpng1637", + "url": SF_PROJECTS + "/libpng/files/libpng16/1.6.39/lpng1639.zip/download", + "filename": "lpng1639.zip", + "dir": "lpng1639", + "license": "LICENSE", "build": [ # lint: do not inline cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), @@ -186,10 +238,25 @@ deps = { "headers": [r"png*.h"], "libs": [r"libpng16.lib"], }, + "brotli": { + "url": "https://github.com/google/brotli/archive/refs/tags/v1.0.9.tar.gz", + "filename": "brotli-1.0.9.tar.gz", + "dir": "brotli-1.0.9", + "license": "LICENSE", + "build": [ + cmd_cmake(), + cmd_nmake(target="clean"), + cmd_nmake(target="brotlicommon-static"), + cmd_nmake(target="brotlidec-static"), + cmd_xcopy(r"c\include", "{inc_dir}"), + ], + "libs": ["*.lib"], + }, "freetype": { "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 "filename": "freetype-2.12.1.tar.gz", "dir": "freetype-2.12.1", + "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { # freetype setting is /MD for .dll and /MT for .lib, we need /MD @@ -198,13 +265,13 @@ deps = { '': '\n $(WindowsSDKVersion)', # noqa: E501 }, r"builds\windows\vc2010\freetype.user.props": { - "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ", # noqa: E501 + "": "FT_CONFIG_OPTION_SYSTEM_ZLIB;FT_CONFIG_OPTION_USE_PNG;FT_CONFIG_OPTION_USE_HARFBUZZ;FT_CONFIG_OPTION_USE_BROTLI", # noqa: E501 "": r"{dir_harfbuzz}\src;{inc_dir}", # noqa: E501 "": "{lib_dir}", # noqa: E501 - "": "zlib.lib;libpng16.lib", # noqa: E501 + "": "zlib.lib;libpng16.lib;brotlicommon-static.lib;brotlidec-static.lib", # noqa: E501 }, r"src/autofit/afshaper.c": { - # link against harfbuzz.lib once it becomes available + # link against harfbuzz.lib "#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ": '#ifdef FT_CONFIG_OPTION_USE_HARFBUZZ\n#pragma comment(lib, "harfbuzz.lib")', # noqa: E501 }, }, @@ -222,9 +289,10 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.13/lcms2-2.13.1.tar.gz/download", - "filename": "lcms2-2.13.1.tar.gz", - "dir": "lcms2-2.13.1", + "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", + "filename": "lcms2-2.14.tar.gz", + "dir": "lcms2-2.14", + "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { # default is /MD for x86 and /MT for x64, we need /MD always @@ -250,8 +318,14 @@ deps = { "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz", "filename": "openjpeg-2.5.0.tar.gz", "dir": "openjpeg-2.5.0", + "license": "LICENSE", + "patch": { + r"src\lib\openjp2\ht_dec.c": { + "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501 + } + }, "build": [ - cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), + cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), cmd_nmake(target="clean"), cmd_nmake(target="openjp2"), cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), @@ -264,6 +338,7 @@ deps = { "url": "https://github.com/ImageOptim/libimagequant/archive/e4c1334be0eff290af5e2b4155057c2953a313ab.zip", # noqa: E501 "filename": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab.zip", "dir": "libimagequant-e4c1334be0eff290af5e2b4155057c2953a313ab", + "license": "COPYRIGHT", "patch": { "CMakeLists.txt": { "if(OPENMP_FOUND)": "if(false)", @@ -281,10 +356,12 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip", - "filename": "harfbuzz-5.3.1.zip", - "dir": "harfbuzz-5.3.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", + "filename": "harfbuzz-6.0.0.zip", + "dir": "harfbuzz-6.0.0", + "license": "COPYING", "build": [ + cmd_set("CXXFLAGS", "-d2FH4-"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), cmd_nmake(target="harfbuzz"), @@ -296,7 +373,9 @@ deps = { "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", "filename": "fribidi-1.0.12.zip", "dir": "fribidi-1.0.12", + "license": "COPYING", "build": [ + cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), cmd_cmake(), cmd_nmake(target="clean"), @@ -392,22 +471,36 @@ def extract_dep(url, filename): raise RuntimeError(ex) print("Extracting " + filename) + sources_dir_abs = os.path.abspath(sources_dir) if filename.endswith(".zip"): with zipfile.ZipFile(file) as zf: + for member in zf.namelist(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member)) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + msg = "Attempted Path Traversal in Zip File" + raise RuntimeError(msg) zf.extractall(sources_dir) elif filename.endswith(".tar.gz") or filename.endswith(".tgz"): with tarfile.open(file, "r:gz") as tgz: + for member in tgz.getnames(): + member_abspath = os.path.abspath(os.path.join(sources_dir, member)) + member_prefix = os.path.commonpath([sources_dir_abs, member_abspath]) + if sources_dir_abs != member_prefix: + msg = "Attempted Path Traversal in Tar File" + raise RuntimeError(msg) tgz.extractall(sources_dir) else: - raise RuntimeError("Unknown archive type: " + filename) + msg = "Unknown archive type: " + filename + raise RuntimeError(msg) def write_script(name, lines): name = os.path.join(build_dir, name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) - with open(name, "w") as f: - f.write("\n\r".join(lines)) + with open(name, "w", newline="") as f: + f.write(os.linesep.join(lines)) if verbose: for line in lines: print(" " + line) @@ -431,6 +524,21 @@ def build_dep(name): extract_dep(dep["url"], dep["filename"]) + licenses = dep["license"] + if isinstance(licenses, str): + licenses = [licenses] + license_text = "" + for license_file in licenses: + with open(os.path.join(sources_dir, dir, license_file)) as f: + license_text += f.read() + if "license_pattern" in dep: + match = re.search(dep["license_pattern"], license_text, re.DOTALL) + license_text = "\n".join(match.groups()) + assert len(license_text) > 50 + with open(os.path.join(license_dir, f"{dir}.txt"), "w") as f: + print(f"Writing license {dir}.txt") + f.write(license_text) + for patch_file, patch_list in dep.get("patch", {}).items(): patch_file = os.path.join(sources_dir, dir, patch_file.format(**prefs)) with open(patch_file) as f: @@ -477,6 +585,7 @@ def build_pillow(): cmd_cd("{pillow_dir}"), *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow + cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501 ] @@ -520,7 +629,8 @@ if __name__ == "__main__": elif arg == "--srcdir": sources_dir = os.path.sep + "src" else: - raise ValueError("Unknown parameter: " + arg) + msg = "Unknown parameter: " + arg + raise ValueError(msg) # dependency cache directory os.makedirs(depends_dir, exist_ok=True) @@ -536,9 +646,8 @@ if __name__ == "__main__": msvs = find_msvs() if msvs is None: - raise RuntimeError( - "Visual Studio not found. Please install Visual Studio 2017 or newer." - ) + msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." + raise RuntimeError(msg) print("Found Visual Studio at:", msvs["vs_dir"]) print("Using output directory:", build_dir) @@ -551,10 +660,12 @@ if __name__ == "__main__": bin_dir = os.path.join(build_dir, "bin") # directory for storing project files sources_dir = build_dir + sources_dir + # copy dependency licenses to this directory + license_dir = os.path.join(build_dir, "license") shutil.rmtree(build_dir, ignore_errors=True) os.makedirs(build_dir, exist_ok=False) - for path in [inc_dir, lib_dir, bin_dir, sources_dir]: + for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: os.makedirs(path, exist_ok=True) prefs = { @@ -572,6 +683,7 @@ if __name__ == "__main__": "lib_dir": lib_dir, "bin_dir": bin_dir, "src_dir": sources_dir, + "license_dir": license_dir, # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically